Compare commits
84 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6b4a211b8 | |||
4c8364328c | |||
5192978917 | |||
4dfae60875 | |||
b7723c91df | |||
3993f97de1 | |||
f0a7fc1303 | |||
8ed22bebf0 | |||
e364b20b1b | |||
305b9237c8 | |||
39c729b420 | |||
d290a2c457 | |||
078682fd48 | |||
56ba989ad8 | |||
667bf76591 | |||
2768e4434a | |||
caf0e44f3e | |||
11c8aade4c | |||
71cb4c8868 | |||
3c817b910a | |||
be2f02560c | |||
e0aa1b2476 | |||
90d54f85d9 | |||
fdca3e37d1 | |||
d2463fd1f3 | |||
854756c5dd | |||
50837411d3 | |||
9e7bd07ae0 | |||
c09c4a0664 | |||
eb5e1cf41d | |||
3e443617e9 | |||
648dacdd1a | |||
7a613ac191 | |||
1414439ceb | |||
036c0cdd5b | |||
ccac1fec15 | |||
bba1913d65 | |||
d24dc4726b | |||
0c0c7f1230 | |||
1c06ffb8a8 | |||
8cfc39e381 | |||
6d89ed657c | |||
3e3017f31a | |||
37dcd67bdc | |||
ffb31bf2b1 | |||
4c8f7fd11d | |||
d07e604cbe | |||
75ec6a49e9 | |||
c3de834f57 | |||
a5cb6dc299 | |||
56af93b14e | |||
45f72cb836 | |||
656edf30a7 | |||
aafb805194 | |||
bd8148f3e2 | |||
4823e68700 | |||
|
4371701ca9 | ||
|
60bf927c67 | ||
|
f79f0db862 | ||
|
7e30db3864 | ||
0987290dca | |||
4e9d98afe2 | |||
ce825b75b1 | |||
0c72f96e16 | |||
fb4fe9d487 | |||
b5e58c4c25 | |||
b611b296c3 | |||
16437b5deb | |||
33bcbece88 | |||
74a086c855 | |||
192748bb1d | |||
d9dee3af7b | |||
2b51aace6e | |||
3ef7861f22 | |||
968c2634b7 | |||
6dd8eea3d3 | |||
|
fe3d040978 | ||
9b955641d0 | |||
77b292e47e | |||
22b79f5376 | |||
|
520c234a94 | ||
|
dacb9ad8fc | ||
6053cfcdac | |||
5ce7ce0542 |
53
README.md
@ -1 +1,52 @@
|
|||||||
# LoLAnalytics.Umbrella
|
# LoLAnalytics
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- Elixir
|
||||||
|
- RabbitMQ
|
||||||
|
- MinIO
|
||||||
|
|
||||||
|
A `docker-compose` file is provided to run them locally.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
The followign environment variables are required:
|
||||||
|
|
||||||
|
```
|
||||||
|
export RIOT_API_KEY="{API-KEY}"
|
||||||
|
|
||||||
|
export EX_AWS_SECRET_KEY="{SECRET}"
|
||||||
|
export EX_AWS_ACCESS_KEY="{ACCESS}"
|
||||||
|
export EX_AWS_ENDPOINT="{HOST}"
|
||||||
|
export EX_AWS_PORT="{PORT}" # minio defaults to 9000
|
||||||
|
export DATABASE_URL="ecto://{USERNAME}:{PASSWORD}@{HOST}/lol-analytics"
|
||||||
|
export SECRET_KEY_BASE="SECRET-KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```
|
||||||
|
mix deps.get
|
||||||
|
mix ecto.create && mix ecto.migrate
|
||||||
|
iex -S mix phx.server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queueing a player
|
||||||
|
|
||||||
|
```
|
||||||
|
Scrapper.Queues.PlayerQueue.enqueue_puuid("PUUID")
|
||||||
|
```
|
||||||
|
|
||||||
|
The scrapper will retrieve player ranked history, enqueue it's teammates, and store every match in minio.
|
||||||
|
|
||||||
|
## Processing
|
||||||
|
|
||||||
|
A `Task` to process the matches stored for a patch can be spawned by running:
|
||||||
|
```
|
||||||
|
LolAnalytics.MatchesProcessor.process_for_patch "14.12.594.4901"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web site
|
||||||
|
|
||||||
|
A web site is exposed at `localhost:4000`.
|
@ -1,3 +0,0 @@
|
|||||||
defmodule LolAnalytics.Analyzer do
|
|
||||||
@callback analyze(:url, path :: String.t()) :: :ok
|
|
||||||
end
|
|
@ -1,50 +0,0 @@
|
|||||||
defmodule LolAnalytics.Analyzer.ChampionAnalyzer do
|
|
||||||
alias Hex.HTTP
|
|
||||||
@behaviour LolAnalytics.Analyzer
|
|
||||||
|
|
||||||
def analyze_all_matches do
|
|
||||||
Storage.MatchStorage.S3MatchStorage.list_files("ranked")
|
|
||||||
|> Enum.map(& &1.key)
|
|
||||||
|> Enum.each(fn path ->
|
|
||||||
LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/#{path}")
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
iex> LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/14.9.580.2108/EUW1_6923309745.json")
|
|
||||||
"""
|
|
||||||
@impl true
|
|
||||||
@spec analyze(atom(), String.t()) :: :ok
|
|
||||||
def analyze(:url, path) do
|
|
||||||
data = HTTPoison.get!(path)
|
|
||||||
analyze(:data, data.body)
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
@spec analyze(atom(), any()) :: list(LoLAPI.Model.Participant.t())
|
|
||||||
def analyze(:data, data) do
|
|
||||||
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
|
|
||||||
participants = decoded_match.info.participants
|
|
||||||
version = extract_game_version(decoded_match)
|
|
||||||
|
|
||||||
participants
|
|
||||||
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
|
|
||||||
if participant.teamPosition != "" do
|
|
||||||
LolAnalytics.ChampionWinRate.ChampionWinRateRepo.add_champion_win_rate(
|
|
||||||
participant.championId,
|
|
||||||
version,
|
|
||||||
participant.teamPosition,
|
|
||||||
participant.win
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp extract_game_version(game_data) do
|
|
||||||
game_data.info.gameVersion
|
|
||||||
|> String.split(".")
|
|
||||||
|> Enum.take(2)
|
|
||||||
|> Enum.join(".")
|
|
||||||
end
|
|
||||||
end
|
|
@ -10,7 +10,10 @@ defmodule LoLAnalytics.Application do
|
|||||||
children = [
|
children = [
|
||||||
LoLAnalytics.Repo,
|
LoLAnalytics.Repo,
|
||||||
{DNSCluster, query: Application.get_env(:lol_analytics, :dns_cluster_query) || :ignore},
|
{DNSCluster, query: Application.get_env(:lol_analytics, :dns_cluster_query) || :ignore},
|
||||||
{Phoenix.PubSub, name: LoLAnalytics.PubSub}
|
{Phoenix.PubSub, name: LoLAnalytics.PubSub},
|
||||||
|
{Task.Supervisor, name: LoLAnalytics.TaskSupervisor},
|
||||||
|
# {LolAnalytics.MatchProcessor.MatchesBroadwayProcessor, []},
|
||||||
|
{LolAnalytics.MatchProcessor.MatchesProducer, []}
|
||||||
# Start a worker by calling: LoLAnalytics.Worker.start_link(arg)
|
# Start a worker by calling: LoLAnalytics.Worker.start_link(arg)
|
||||||
# {LoLAnalytics.Worker, arg}
|
# {LoLAnalytics.Worker, arg}
|
||||||
]
|
]
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateRepo do
|
|
||||||
import Ecto.Query
|
|
||||||
alias LolAnalytics.ChampionWinRate.ChampionWinRateSchema
|
|
||||||
alias LoLAnalytics.Repo
|
|
||||||
|
|
||||||
@spec add_champion_win_rate(
|
|
||||||
champion_id :: String.t(),
|
|
||||||
patch :: String.t(),
|
|
||||||
position :: String.t(),
|
|
||||||
win? :: boolean
|
|
||||||
) :: {:ok, ChampionWinRateSchema.t()} | {:error, Ecto.Changeset.t()}
|
|
||||||
def add_champion_win_rate(champion_id, patch, position, win?) do
|
|
||||||
Repo.transaction(fn ->
|
|
||||||
champion_query =
|
|
||||||
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
|
|
||||||
where: cwr.champion_id == ^champion_id and cwr.position == ^position,
|
|
||||||
lock: "FOR UPDATE"
|
|
||||||
|
|
||||||
champion_data = Repo.one(champion_query)
|
|
||||||
|
|
||||||
case champion_data do
|
|
||||||
nil ->
|
|
||||||
ChampionWinRateSchema.changeset(%ChampionWinRateSchema{}, %{
|
|
||||||
champion_id: champion_id,
|
|
||||||
patch: patch,
|
|
||||||
total_games: 1,
|
|
||||||
position: position,
|
|
||||||
total_wins: if(win?, do: 1, else: 0)
|
|
||||||
})
|
|
||||||
|> Repo.insert!()
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
total_games = champion_data.total_games + 1
|
|
||||||
total_wins = champion_data.total_wins + if win?, do: 1, else: 0
|
|
||||||
|
|
||||||
ChampionWinRateSchema.changeset(champion_data, %{
|
|
||||||
total_games: total_games,
|
|
||||||
total_wins: total_wins
|
|
||||||
})
|
|
||||||
|> Repo.update!()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_win_rates() do
|
|
||||||
Repo.all(ChampionWinRateSchema)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_champion_win_rate(champion_id, patch) do
|
|
||||||
champion_query =
|
|
||||||
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
|
|
||||||
where: cwr.champion_id == ^champion_id
|
|
||||||
|
|
||||||
Repo.one(champion_query)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,20 +0,0 @@
|
|||||||
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateSchema do
|
|
||||||
use Ecto.Schema
|
|
||||||
import Ecto.Changeset
|
|
||||||
|
|
||||||
schema "champion_win_rate" do
|
|
||||||
field :champion_id, :integer
|
|
||||||
field :total_games, :integer
|
|
||||||
field :patch, :string
|
|
||||||
field :position, :string
|
|
||||||
field :total_wins, :integer
|
|
||||||
|
|
||||||
timestamps()
|
|
||||||
end
|
|
||||||
|
|
||||||
def changeset(%__MODULE__{} = champion_win_rate, attrs) do
|
|
||||||
champion_win_rate
|
|
||||||
|> cast(attrs, [:champion_id, :total_games, :patch, :total_wins, :position])
|
|
||||||
|> validate_required([:champion_id, :total_games, :patch, :total_wins, :position])
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,41 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Champion.ChampionMetadata do
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||||
|
@champions_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/champion.json"
|
||||||
|
|
||||||
|
def update_metadata() do
|
||||||
|
{:ok, %{"data" => data}} = get_champions()
|
||||||
|
|
||||||
|
data
|
||||||
|
|> Enum.each(&save_metadata/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_champions() do
|
||||||
|
with {:ok, resp} <- HTTPoison.get(@champions_data_url),
|
||||||
|
data <- Poison.decode(resp.body) do
|
||||||
|
data
|
||||||
|
else
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_metadata({_champion, info}) do
|
||||||
|
%{
|
||||||
|
"image" => %{
|
||||||
|
"full" => full_image
|
||||||
|
},
|
||||||
|
"name" => name,
|
||||||
|
"key" => key_string
|
||||||
|
} = info
|
||||||
|
|
||||||
|
attrs = %{
|
||||||
|
image: full_image,
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
|
||||||
|
{champion_id, _} = Integer.parse(key_string)
|
||||||
|
|
||||||
|
ChampionRepo.update(champion_id, attrs)
|
||||||
|
|
||||||
|
info
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,29 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Champion.ChampionRepo do
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||||
|
|
||||||
|
@spec get_or_create(String.t()) :: struct()
|
||||||
|
def get_or_create(champion_id) do
|
||||||
|
champion = Repo.get_by(ChampionSchema, champion_id: champion_id)
|
||||||
|
|
||||||
|
case champion do
|
||||||
|
nil ->
|
||||||
|
changeset = ChampionSchema.changeset(%ChampionSchema{}, %{champion_id: champion_id})
|
||||||
|
Repo.insert(changeset)
|
||||||
|
|
||||||
|
champion ->
|
||||||
|
champion
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(champion_id, attrs) do
|
||||||
|
get_or_create(champion_id)
|
||||||
|
|> ChampionSchema.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec list_champions() :: any()
|
||||||
|
def list_champions() do
|
||||||
|
Repo.all(ChampionSchema)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Champion.ChampionSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "dim_champion" do
|
||||||
|
field :champion_id, :integer
|
||||||
|
field :name, :string
|
||||||
|
field :image, :string
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(champion = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
champion
|
||||||
|
|> cast(attrs, [:champion_id, :name, :image])
|
||||||
|
|> validate_required([:champion_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Item.ItemMetadata do
|
||||||
|
alias LolAnalytics.Dimensions.Item.ItemRepo
|
||||||
|
@items_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/item.json"
|
||||||
|
|
||||||
|
def update_metadata() do
|
||||||
|
get_items()
|
||||||
|
|> Enum.each(&save_metadata/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_items() do
|
||||||
|
with {:ok, resp} <- HTTPoison.get(@items_data_url),
|
||||||
|
%{"data" => data} <- Poison.decode!(resp.body) do
|
||||||
|
data
|
||||||
|
else
|
||||||
|
_ -> {:error, :get_items_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_metadata({item_id, metadata}) do
|
||||||
|
ItemRepo.update(item_id, %{metadata: metadata})
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Item.ItemRepo do
|
||||||
|
alias LolAnalytics.Dimensions.Item.ItemSchema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def get_or_create(item_id) do
|
||||||
|
query = from i in ItemSchema, where: i.item_id == ^item_id
|
||||||
|
|
||||||
|
case Repo.one(query) do
|
||||||
|
nil ->
|
||||||
|
item_changeset = ItemSchema.changeset(%ItemSchema{}, %{item_id: item_id})
|
||||||
|
Repo.insert!(item_changeset)
|
||||||
|
|
||||||
|
item ->
|
||||||
|
item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_items() do
|
||||||
|
Repo.all(ItemSchema)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec update(item_id :: String.t(), attrs :: map()) :: any()
|
||||||
|
def update(item_id, attrs) do
|
||||||
|
get_or_create(item_id)
|
||||||
|
|> ItemSchema.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,18 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Item.ItemSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@args [:item_id, :metadata]
|
||||||
|
|
||||||
|
schema "dim_item" do
|
||||||
|
field :item_id, :integer
|
||||||
|
field :metadata, :map
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(item = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
item
|
||||||
|
|> cast(attrs, @args)
|
||||||
|
|> validate_required([:item_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,85 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Match.MatchRepo do
|
||||||
|
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
@spec get_or_create(%{
|
||||||
|
:match_id => String.t(),
|
||||||
|
:queue_id => integer(),
|
||||||
|
:patch_number => String.t()
|
||||||
|
}) :: %MatchSchema{}
|
||||||
|
def get_or_create(%{match_id: match_id, queue_id: queue_id, patch_number: patch_number}) do
|
||||||
|
_patch = PatchRepo.get_or_create(patch_number)
|
||||||
|
query = from m in MatchSchema, where: m.match_id == ^match_id
|
||||||
|
match = Repo.one(query)
|
||||||
|
|
||||||
|
case match do
|
||||||
|
nil ->
|
||||||
|
%MatchSchema{}
|
||||||
|
|> MatchSchema.changeset(%{
|
||||||
|
match_id: match_id,
|
||||||
|
patch_number: patch_number,
|
||||||
|
queue_id: queue_id,
|
||||||
|
fact_champion_played_game_status: 0,
|
||||||
|
fact_champion_picked_item_status: 0,
|
||||||
|
fact_champion_picked_summoner_spell_status: 0
|
||||||
|
})
|
||||||
|
|> Repo.insert!()
|
||||||
|
|
||||||
|
match ->
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get(String.t()) :: nil | %MatchSchema{}
|
||||||
|
def get(match_id) do
|
||||||
|
query = from m in MatchSchema, where: m.match_id == ^match_id
|
||||||
|
|
||||||
|
Repo.one(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
@type update_attrs :: %{
|
||||||
|
optional(:fact_champion_played_game_status) => process_status(),
|
||||||
|
optional(:fact_champion_picked_item_status) => process_status(),
|
||||||
|
optional(:fact_champion_picked_summoner_spell_status) => process_status()
|
||||||
|
}
|
||||||
|
@spec update(%MatchSchema{}, update_attrs()) :: %MatchSchema{}
|
||||||
|
def update(match, attrs) do
|
||||||
|
mapped_attrs =
|
||||||
|
attrs
|
||||||
|
|> Enum.map(fn {k, v} -> {k, process_status_atom_to_db(v)} end)
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
match
|
||||||
|
|> MatchSchema.changeset(mapped_attrs)
|
||||||
|
|> Repo.update!()
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_matches() do
|
||||||
|
Repo.all(MatchSchema)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_unprocessed_matches(limit, queue \\ 420) do
|
||||||
|
query =
|
||||||
|
from m in MatchSchema,
|
||||||
|
where:
|
||||||
|
(m.fact_champion_picked_item_status == 0 or
|
||||||
|
m.fact_champion_picked_summoner_spell_status == 0 or
|
||||||
|
m.fact_champion_played_game_status == 0) and
|
||||||
|
m.queue_id == ^queue,
|
||||||
|
order_by: [desc: m.updated_at],
|
||||||
|
limit: ^limit
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
@type process_status :: :not_processed | :processed | :error
|
||||||
|
defp process_status_atom_to_db(:not_processed), do: 0
|
||||||
|
defp process_status_atom_to_db(:enqueued), do: 1
|
||||||
|
defp process_status_atom_to_db(:processed), do: 2
|
||||||
|
defp process_status_atom_to_db(:error), do: 3
|
||||||
|
defp process_status_atom_to_db(:error_match_not_found), do: 4
|
||||||
|
defp process_status_atom_to_db(_), do: raise("Invalid processing status")
|
||||||
|
end
|
@ -0,0 +1,29 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Match.MatchSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@casting_attrs [
|
||||||
|
:match_id,
|
||||||
|
:queue_id,
|
||||||
|
:patch_number,
|
||||||
|
:fact_champion_picked_item_status,
|
||||||
|
:fact_champion_picked_summoner_spell_status,
|
||||||
|
:fact_champion_played_game_status
|
||||||
|
]
|
||||||
|
|
||||||
|
schema "dim_match" do
|
||||||
|
field :match_id, :string
|
||||||
|
field :patch_number, :string
|
||||||
|
field :queue_id, :integer
|
||||||
|
field :fact_champion_picked_item_status, :integer
|
||||||
|
field :fact_champion_picked_summoner_spell_status, :integer
|
||||||
|
field :fact_champion_played_game_status, :integer
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(match = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
match
|
||||||
|
|> cast(attrs, @casting_attrs)
|
||||||
|
|> validate_required([:match_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Patch.PatchRepo do
|
||||||
|
alias LolAnalytics.Dimensions.Patch.PatchSchema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def get_or_create(patch_number) do
|
||||||
|
query = from p in PatchSchema, where: p.patch_number == ^patch_number
|
||||||
|
|
||||||
|
case Repo.one(query) do
|
||||||
|
nil ->
|
||||||
|
patch_changeset =
|
||||||
|
PatchSchema.changeset(
|
||||||
|
%PatchSchema{},
|
||||||
|
%{patch_number: patch_number}
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert(patch_changeset)
|
||||||
|
|
||||||
|
patch ->
|
||||||
|
patch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_patches() do
|
||||||
|
Repo.all(PatchSchema)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,16 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Patch.PatchSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "dim_patch" do
|
||||||
|
field :patch_number, :string
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(patch = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
patch
|
||||||
|
|> cast(attrs, [:patch_number])
|
||||||
|
|> validate_required([:patch_number])
|
||||||
|
|> unique_constraint([:patch_number])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Player.PlayerRepo do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Player.PlayerSchema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
def get_or_create(puuid) do
|
||||||
|
query = from p in PlayerSchema, where: p.puuid == ^puuid
|
||||||
|
|
||||||
|
case Repo.one(query) do
|
||||||
|
nil ->
|
||||||
|
player_changeset =
|
||||||
|
PlayerSchema.changeset(
|
||||||
|
%PlayerSchema{},
|
||||||
|
%{puuid: puuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert(player_changeset)
|
||||||
|
|
||||||
|
player ->
|
||||||
|
player
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_players() do
|
||||||
|
Repo.all(PlayerSchema)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.Player.PlayerSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@attrs [:puuid, :last_processed_at]
|
||||||
|
|
||||||
|
schema "dim_player" do
|
||||||
|
field :puuid, :string
|
||||||
|
|
||||||
|
field :last_processed_at, :utc_datetime,
|
||||||
|
default: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(player = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
player
|
||||||
|
|> cast(attrs, @attrs)
|
||||||
|
|> validate_required([:puuid])
|
||||||
|
|> unique_constraint([:puuid])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellMetadata do
|
||||||
|
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
|
||||||
|
@spells_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/summoner.json"
|
||||||
|
|
||||||
|
def update_metadata() do
|
||||||
|
get_spells()
|
||||||
|
|> Enum.each(&save_metadata/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_spells() do
|
||||||
|
case HTTPoison.get(@spells_url) do
|
||||||
|
{:ok, resp} ->
|
||||||
|
Poison.decode!(resp.body)["data"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_metadata({name, metadata}) do
|
||||||
|
%{"key" => spell_id} = metadata
|
||||||
|
|
||||||
|
SummonerSpellRepo.update(spell_id, %{metadata: metadata})
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,37 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
@spec get_or_create(String.t()) :: any()
|
||||||
|
def get_or_create(spell_id) do
|
||||||
|
query = from s in SummonerSpellSchema, where: s.spell_id == ^spell_id
|
||||||
|
spell = Repo.one(query)
|
||||||
|
|
||||||
|
case spell do
|
||||||
|
nil ->
|
||||||
|
spell_changeset =
|
||||||
|
SummonerSpellSchema.changeset(
|
||||||
|
%SummonerSpellSchema{},
|
||||||
|
%{spell_id: spell_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert(spell_changeset)
|
||||||
|
|
||||||
|
spell ->
|
||||||
|
spell
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec update(spell_id :: String.t(), attrs :: map()) :: any()
|
||||||
|
def update(spell_id, attrs) do
|
||||||
|
get_or_create(spell_id)
|
||||||
|
|> SummonerSpellSchema.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_spells() do
|
||||||
|
Repo.all(SummonerSpellSchema)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,16 @@
|
|||||||
|
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "dim_summoner_spell" do
|
||||||
|
field :spell_id, :integer
|
||||||
|
field :metadata, :map
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(summoner_spell = %__MODULE__{}, attrs) do
|
||||||
|
summoner_spell
|
||||||
|
|> cast(attrs, [:spell_id, :metadata])
|
||||||
|
|> validate_required([:spell_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,87 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedItem.FactProcessor do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||||
|
alias LolAnalytics.Facts.ChampionPickedItem.Repo
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
|
||||||
|
iex> LolAnalytics.Facts.ChampionPickedItem.FactProcessor.process_game_at_url("http://192.168.1.55:9000/ranked/14.3.558.106/EUW1_6803789466.json")
|
||||||
|
"""
|
||||||
|
@impl true
|
||||||
|
def process_game_at_url(url) do
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||||
|
process_game_data(decoded_match)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
Logger.error("Could not process data from #{url} for ChampionPickedItem")
|
||||||
|
{:error, "Could not process data from #{url}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||||
|
def process_match(match) do
|
||||||
|
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(match_url),
|
||||||
|
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||||
|
process_game_data(decoded_match)
|
||||||
|
MatchRepo.update(match, %{fact_champion_picked_item_status: :processed})
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
MatchRepo.update(match, fact_champion_picked_item_status: :error_match_not_found)
|
||||||
|
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||||
|
{:error, "Could not process data from #{match_url}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_game_data(decoded_match) do
|
||||||
|
participants = decoded_match.info.participants
|
||||||
|
version = extract_game_version(decoded_match)
|
||||||
|
|
||||||
|
match =
|
||||||
|
MatchRepo.get_or_create(%{
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
patch_number: decoded_match.info.gameVersion,
|
||||||
|
queue_id: decoded_match.info.queueId
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.info("Processing ChampionPickedItem for match #{decoded_match.metadata.matchId}")
|
||||||
|
|
||||||
|
participants
|
||||||
|
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
|
||||||
|
if participant.teamPosition != "" do
|
||||||
|
[:item0, :item1, :item2, :item3, :item4, :item5, :item6]
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.each(fn {item_key, index} ->
|
||||||
|
item_key = Map.get(participant, item_key)
|
||||||
|
|
||||||
|
Repo.insert(%{
|
||||||
|
champion_id: participant.championId,
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
is_win: participant.win,
|
||||||
|
item_id: item_key,
|
||||||
|
slot_number: index,
|
||||||
|
game_length_seconds: decoded_match.info.gameDuration,
|
||||||
|
queue_id: decoded_match.info.queueId,
|
||||||
|
puuid: participant.puuid,
|
||||||
|
team_position: participant.teamPosition,
|
||||||
|
patch_number: version
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_game_version(game_data) do
|
||||||
|
game_data.info.gameVersion
|
||||||
|
|> String.split(".")
|
||||||
|
|> Enum.take(2)
|
||||||
|
|> Enum.join(".")
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,89 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedItem.Repo do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||||
|
alias LolAnalytics.Dimensions.Item.ItemSchema
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||||
|
alias LolAnalytics.Facts.ChampionPickedItem.Schema
|
||||||
|
alias LolAnalytics.Dimensions.Item.ItemRepo
|
||||||
|
alias LolAnalytics.Dimensions.Player.PlayerRepo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Inserts a new entry for the campion_picked_item fact.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
iex> item_picked = %{champion_id: 90, item_id: 1, match_id: "123", patch_number: "14.9", puuid: "1223", slot_number: 1, queue_id: 420, team_position: "JUNGLE", slot_number: 1, is_win: true}
|
||||||
|
iex> LolAnalytics.Facts.ChampionPickedItem.Repo.insert(item_picked)
|
||||||
|
"""
|
||||||
|
@spec insert(%{
|
||||||
|
:champion_id => integer(),
|
||||||
|
:item_id => integer(),
|
||||||
|
:match_id => binary(),
|
||||||
|
:patch_number => String.t(),
|
||||||
|
:puuid => String.t(),
|
||||||
|
:is_win => boolean(),
|
||||||
|
:queue_id => integer(),
|
||||||
|
:team_position => String.t(),
|
||||||
|
:slot_number => integer()
|
||||||
|
}) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def insert(attrs) do
|
||||||
|
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||||
|
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||||
|
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||||
|
_item_id = ItemRepo.get_or_create(attrs.item_id)
|
||||||
|
|
||||||
|
prev =
|
||||||
|
from(f in Schema,
|
||||||
|
where:
|
||||||
|
f.match_id == ^attrs.match_id and
|
||||||
|
f.puuid == ^attrs.puuid and
|
||||||
|
f.item_id == ^attrs.item_id and
|
||||||
|
f.slot_number == ^attrs.slot_number
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
|
||||||
|
Schema.changeset(prev || %Schema{}, attrs)
|
||||||
|
|> Repo.insert_or_update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_champion_picked_items(String.t(), String.t(), String.t()) :: list()
|
||||||
|
def get_champion_picked_items(champion_id, team_position, patch_number) do
|
||||||
|
query =
|
||||||
|
from f in Schema,
|
||||||
|
where:
|
||||||
|
f.champion_id == ^champion_id and
|
||||||
|
f.team_position == ^team_position and
|
||||||
|
f.item_id != 0 and f.patch_number == ^patch_number,
|
||||||
|
join: c in ChampionSchema,
|
||||||
|
on: c.champion_id == f.champion_id,
|
||||||
|
join: i in ItemSchema,
|
||||||
|
on: i.item_id == f.item_id,
|
||||||
|
select: %{
|
||||||
|
wins: fragment("count(CASE WHEN ? THEN 1 END)", f.is_win),
|
||||||
|
win_rate:
|
||||||
|
fragment(
|
||||||
|
"
|
||||||
|
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||||
|
)",
|
||||||
|
f.is_win
|
||||||
|
),
|
||||||
|
metadata: i.metadata,
|
||||||
|
item_id: i.item_id,
|
||||||
|
champion_id: c.champion_id,
|
||||||
|
team_position: f.team_position,
|
||||||
|
total_games: count("*")
|
||||||
|
},
|
||||||
|
group_by: [
|
||||||
|
i.metadata,
|
||||||
|
i.item_id,
|
||||||
|
c.champion_id,
|
||||||
|
f.team_position
|
||||||
|
]
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,34 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedItem.Schema do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@casting_args [
|
||||||
|
:champion_id,
|
||||||
|
:item_id,
|
||||||
|
:match_id,
|
||||||
|
:is_win,
|
||||||
|
:queue_id,
|
||||||
|
:patch_number,
|
||||||
|
:team_position,
|
||||||
|
:puuid,
|
||||||
|
:slot_number
|
||||||
|
]
|
||||||
|
|
||||||
|
schema "fact_champion_picked_item" do
|
||||||
|
field :champion_id, :integer
|
||||||
|
field :item_id, :integer
|
||||||
|
field :match_id, :string
|
||||||
|
field :is_win, :boolean
|
||||||
|
field :queue_id, :integer
|
||||||
|
field :patch_number, :string
|
||||||
|
field :team_position, :string
|
||||||
|
field :puuid, :string
|
||||||
|
field :slot_number, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(fact, attrs) do
|
||||||
|
fact
|
||||||
|
|> cast(attrs, @casting_args)
|
||||||
|
|> validate_required(@casting_args)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,78 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.FactProcessor do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||||
|
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
|
||||||
|
|
||||||
|
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||||
|
def process_match(match) do
|
||||||
|
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(match_url),
|
||||||
|
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||||
|
process_game_data(decoded_match)
|
||||||
|
MatchRepo.update(match, %{fact_champion_picked_summoner_spell_status: :processed})
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
MatchRepo.update(match, fact_champion_picked_summoner_spell_status: :error_match_not_found)
|
||||||
|
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||||
|
{:error, "Could not process data from #{match_url}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_game_data(decoded_match) do
|
||||||
|
participants = decoded_match.info.participants
|
||||||
|
version = extract_game_version(decoded_match)
|
||||||
|
|
||||||
|
match =
|
||||||
|
MatchRepo.get_or_create(%{
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
patch_number: decoded_match.info.gameVersion,
|
||||||
|
queue_id: decoded_match.info.queueId
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.info("Processing ChampionPickedSummoner for match #{decoded_match.metadata.matchId}")
|
||||||
|
|
||||||
|
participants
|
||||||
|
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
|
||||||
|
if participant.teamPosition != "" do
|
||||||
|
attrs_spell_1 = %{
|
||||||
|
champion_id: participant.championId,
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
is_win: participant.win,
|
||||||
|
summoner_spell_id: participant.summoner1Id,
|
||||||
|
game_length_seconds: decoded_match.info.gameDuration,
|
||||||
|
queue_id: decoded_match.info.queueId,
|
||||||
|
puuid: participant.puuid,
|
||||||
|
team_position: participant.teamPosition,
|
||||||
|
patch_number: version
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs_spell_2 = %{
|
||||||
|
champion_id: participant.championId,
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
is_win: participant.win,
|
||||||
|
summoner_spell_id: participant.summoner2Id,
|
||||||
|
game_length_seconds: decoded_match.info.gameDuration,
|
||||||
|
queue_id: decoded_match.info.queueId,
|
||||||
|
puuid: participant.puuid,
|
||||||
|
team_position: participant.teamPosition,
|
||||||
|
patch_number: version
|
||||||
|
}
|
||||||
|
|
||||||
|
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_1)
|
||||||
|
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_2)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_game_version(game_data) do
|
||||||
|
game_data.info.gameVersion
|
||||||
|
|> String.split(".")
|
||||||
|
|> Enum.take(2)
|
||||||
|
|> Enum.join(".")
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,81 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||||
|
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
|
||||||
|
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||||
|
|
||||||
|
alias LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Player.PlayerRepo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||||
|
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
@spec insert(%{
|
||||||
|
:champion_id => String.t(),
|
||||||
|
:match_id => String.t(),
|
||||||
|
:patch_number => String.t(),
|
||||||
|
:puuid => any(),
|
||||||
|
:summoner_spell_id => String.t()
|
||||||
|
}) :: any()
|
||||||
|
def insert(attrs) do
|
||||||
|
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||||
|
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||||
|
_spell = SummonerSpellRepo.get_or_create(attrs.summoner_spell_id)
|
||||||
|
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||||
|
|
||||||
|
prev =
|
||||||
|
from(f in Schema,
|
||||||
|
where:
|
||||||
|
f.match_id == ^attrs.match_id and
|
||||||
|
f.champion_id == ^attrs.champion_id and
|
||||||
|
f.summoner_spell_id ==
|
||||||
|
^attrs.summoner_spell_id
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
|
||||||
|
Schema.changeset(prev || %Schema{}, attrs)
|
||||||
|
|> Repo.insert_or_update()
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_champion_picked_summoners(String.t(), String.t(), String.t()) :: list()
|
||||||
|
def get_champion_picked_summoners(champion_id, team_position, patch_number) do
|
||||||
|
query =
|
||||||
|
from f in Schema,
|
||||||
|
where:
|
||||||
|
f.champion_id == ^champion_id and
|
||||||
|
f.team_position == ^team_position and
|
||||||
|
f.patch_number == ^patch_number,
|
||||||
|
join: c in ChampionSchema,
|
||||||
|
on: c.champion_id == f.champion_id,
|
||||||
|
join: s in SummonerSpellSchema,
|
||||||
|
on: s.spell_id == f.summoner_spell_id,
|
||||||
|
select: %{
|
||||||
|
wins: fragment("count(CASE WHEN ? THEN 1 END)", f.is_win),
|
||||||
|
win_rate:
|
||||||
|
fragment(
|
||||||
|
"
|
||||||
|
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||||
|
)",
|
||||||
|
f.is_win
|
||||||
|
),
|
||||||
|
spell_id: f.summoner_spell_id,
|
||||||
|
metadata: s.metadata,
|
||||||
|
champion_id: c.champion_id,
|
||||||
|
team_position: f.team_position,
|
||||||
|
total_games: count("*")
|
||||||
|
},
|
||||||
|
group_by: [
|
||||||
|
f.champion_id,
|
||||||
|
f.summoner_spell_id,
|
||||||
|
s.metadata,
|
||||||
|
c.champion_id,
|
||||||
|
f.team_position
|
||||||
|
]
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,36 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@params [
|
||||||
|
:champion_id,
|
||||||
|
:summoner_spell_id,
|
||||||
|
:match_id,
|
||||||
|
:patch_number,
|
||||||
|
:is_win,
|
||||||
|
:game_length_seconds,
|
||||||
|
:queue_id,
|
||||||
|
:team_position,
|
||||||
|
:puuid
|
||||||
|
]
|
||||||
|
|
||||||
|
schema "fact_champion_picked_summoner_spell" do
|
||||||
|
field :champion_id, :integer
|
||||||
|
field :summoner_spell_id, :integer
|
||||||
|
field :match_id, :string
|
||||||
|
field :patch_number, :string
|
||||||
|
field :is_win, :boolean
|
||||||
|
field :game_length_seconds, :integer
|
||||||
|
field :queue_id, :integer
|
||||||
|
field :team_position, :string
|
||||||
|
field :puuid, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
fact
|
||||||
|
|> cast(attrs, @params)
|
||||||
|
|> validate_required(@params)
|
||||||
|
|> unique_constraint([:puuid, :match_id, :summoner_spell_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,136 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||||
|
alias LolAnalytics.Dimensions.Player.PlayerRepo
|
||||||
|
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||||
|
alias LolAnalytics.Facts.ChampionPlayedGame.Schema
|
||||||
|
alias LoLAnalytics.Repo
|
||||||
|
|
||||||
|
def insert(attrs) do
|
||||||
|
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||||
|
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||||
|
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||||
|
|
||||||
|
prev =
|
||||||
|
from(f in Schema,
|
||||||
|
where:
|
||||||
|
f.match_id == ^attrs.match_id and
|
||||||
|
f.champion_id == ^attrs.champion_id and f.queue_id == ^attrs.queue_id
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
|
||||||
|
changeset = Schema.changeset(prev || %Schema{}, attrs)
|
||||||
|
|
||||||
|
Repo.insert_or_update(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_played_matches() do
|
||||||
|
Repo.all(Schema)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_win_rates(opts \\ []) do
|
||||||
|
team_position = Keyword.get(opts, :team_position)
|
||||||
|
patch = Keyword.get(opts, :patch_number, "14.12")
|
||||||
|
|
||||||
|
case {team_position, patch} do
|
||||||
|
{nil, nil} ->
|
||||||
|
get_win_rates()
|
||||||
|
|
||||||
|
{"all", patch} ->
|
||||||
|
get_win_rates_for_patch(patch)
|
||||||
|
|
||||||
|
{nil, patch} ->
|
||||||
|
get_win_rates_for_patch(patch)
|
||||||
|
|
||||||
|
{team_position, patch} ->
|
||||||
|
get_win_rates_for_patch_and_team_position(patch, team_position)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec champion_win_rates_by_patch(String.t(), String.t()) :: [map()]
|
||||||
|
def champion_win_rates_by_patch(champion_id, team_position) do
|
||||||
|
query =
|
||||||
|
from m in Schema,
|
||||||
|
join: c in ChampionSchema,
|
||||||
|
on: c.champion_id == m.champion_id,
|
||||||
|
select: %{
|
||||||
|
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||||
|
win_rate:
|
||||||
|
fragment(
|
||||||
|
"
|
||||||
|
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||||
|
)",
|
||||||
|
m.is_win
|
||||||
|
),
|
||||||
|
id: m.champion_id,
|
||||||
|
patch_number: m.patch_number,
|
||||||
|
name: c.name,
|
||||||
|
image: c.image,
|
||||||
|
team_position: m.team_position,
|
||||||
|
total_games: count("*")
|
||||||
|
},
|
||||||
|
group_by: [m.champion_id, c.image, c.name, m.patch_number, m.team_position],
|
||||||
|
where: m.team_position == ^team_position and m.champion_id == ^champion_id,
|
||||||
|
having: count("*") > 50
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_win_rates_for_patch(patch_number) do
|
||||||
|
query =
|
||||||
|
from m in Schema,
|
||||||
|
join: c in ChampionSchema,
|
||||||
|
on: c.champion_id == m.champion_id,
|
||||||
|
select: %{
|
||||||
|
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||||
|
win_rate:
|
||||||
|
fragment(
|
||||||
|
"
|
||||||
|
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||||
|
)",
|
||||||
|
m.is_win
|
||||||
|
),
|
||||||
|
id: m.champion_id,
|
||||||
|
patch_number: m.patch_number,
|
||||||
|
name: c.name,
|
||||||
|
image: c.image,
|
||||||
|
team_position: m.team_position,
|
||||||
|
total_games: count("*")
|
||||||
|
},
|
||||||
|
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
|
||||||
|
having: m.patch_number == ^patch_number and count("*") > 100
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_win_rates_for_patch_and_team_position(patch_number, team_position) do
|
||||||
|
query =
|
||||||
|
from m in Schema,
|
||||||
|
join: c in ChampionSchema,
|
||||||
|
on: c.champion_id == m.champion_id,
|
||||||
|
select: %{
|
||||||
|
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||||
|
win_rate:
|
||||||
|
fragment(
|
||||||
|
"
|
||||||
|
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||||
|
)",
|
||||||
|
m.is_win
|
||||||
|
),
|
||||||
|
id: m.champion_id,
|
||||||
|
patch_number: m.patch_number,
|
||||||
|
name: c.name,
|
||||||
|
image: c.image,
|
||||||
|
team_position: m.team_position,
|
||||||
|
total_games: count("*")
|
||||||
|
},
|
||||||
|
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
|
||||||
|
having:
|
||||||
|
m.team_position == ^team_position and m.patch_number == ^patch_number and
|
||||||
|
count("*") > 100
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,35 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPlayedGame.Schema do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@casting_attrs [
|
||||||
|
:champion_id,
|
||||||
|
:match_id,
|
||||||
|
:patch_number,
|
||||||
|
:is_win,
|
||||||
|
:game_length_seconds,
|
||||||
|
:team_position,
|
||||||
|
:puuid,
|
||||||
|
:queue_id
|
||||||
|
]
|
||||||
|
|
||||||
|
schema "fact_champion_played_game" do
|
||||||
|
field :champion_id, :integer
|
||||||
|
field :match_id, :string
|
||||||
|
field :patch_number, :string
|
||||||
|
field :is_win, :boolean
|
||||||
|
field :game_length_seconds, :integer
|
||||||
|
field :team_position, :string
|
||||||
|
field :puuid, :string
|
||||||
|
field :queue_id, :integer
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
|
||||||
|
fact
|
||||||
|
|> cast(attrs, @casting_attrs)
|
||||||
|
|> validate_required(@casting_attrs)
|
||||||
|
|> unique_constraint([:id, :champion_id, :queue_id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,63 @@
|
|||||||
|
defmodule LolAnalytics.Facts.ChampionPlayedGame.FactProcessor do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||||
|
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||||
|
|
||||||
|
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||||
|
def process_match(match) do
|
||||||
|
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(match_url),
|
||||||
|
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||||
|
process_game_data(decoded_match)
|
||||||
|
MatchRepo.update(match, %{fact_champion_played_game_status: :processed})
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
MatchRepo.update(match, fact_champion_played_game_status: :error_match_not_found)
|
||||||
|
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||||
|
{:error, "Could not process data from #{match_url}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_game_data(decoded_match) do
|
||||||
|
participants = decoded_match.info.participants
|
||||||
|
version = extract_game_version(decoded_match)
|
||||||
|
|
||||||
|
match =
|
||||||
|
MatchRepo.get_or_create(%{
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
patch_number: decoded_match.info.gameVersion,
|
||||||
|
queue_id: decoded_match.info.queueId
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.info("Processing ChampionPlayedMatch for #{decoded_match.metadata.matchId}")
|
||||||
|
|
||||||
|
participants
|
||||||
|
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
|
||||||
|
if participant.teamPosition != "" do
|
||||||
|
attrs = %{
|
||||||
|
champion_id: participant.championId,
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
is_win: participant.win,
|
||||||
|
game_length_seconds: decoded_match.info.gameDuration,
|
||||||
|
queue_id: decoded_match.info.queueId,
|
||||||
|
puuid: participant.puuid,
|
||||||
|
team_position: participant.teamPosition,
|
||||||
|
patch_number: version
|
||||||
|
}
|
||||||
|
|
||||||
|
LolAnalytics.Facts.ChampionPlayedGame.Repo.insert(attrs)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_game_version(game_data) do
|
||||||
|
game_data.info.gameVersion
|
||||||
|
|> String.split(".")
|
||||||
|
|> Enum.take(2)
|
||||||
|
|> Enum.join(".")
|
||||||
|
end
|
||||||
|
end
|
24
apps/lol_analytics/lib/lol_analytics/facts/facts_runner.ex
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
defmodule LolAnalytics.Facts.FactsRunner do
|
||||||
|
alias LolAnalytics.Facts
|
||||||
|
|
||||||
|
def analyze_match(match) do
|
||||||
|
get_facts()
|
||||||
|
|> Enum.each(fn fact_runner ->
|
||||||
|
apply(fact_runner, [match])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_facts() do
|
||||||
|
[
|
||||||
|
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_match/1,
|
||||||
|
&Facts.ChampionPlayedGame.FactProcessor.process_match/1,
|
||||||
|
&Facts.ChampionPickedItem.FactProcessor.process_match/1
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def peach(enum, fun, concurrency \\ System.schedulers_online(), timeout \\ :infinity) do
|
||||||
|
Task.async_stream(enum, &fun.(&1), max_concurrency: concurrency, timeout: timeout)
|
||||||
|
|> Stream.each(fn {:ok, val} -> val end)
|
||||||
|
|> Enum.to_list()
|
||||||
|
end
|
||||||
|
end
|
@ -1,35 +0,0 @@
|
|||||||
defmodule LolAnalytics.Match.MatchRepo do
|
|
||||||
alias LolAnalytics.Match.MatchSchema
|
|
||||||
|
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
def list_matches do
|
|
||||||
query = from m in MatchSchema, order_by: [desc: m.match_id]
|
|
||||||
LoLAnalytics.Repo.all(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
def number_of_matches do
|
|
||||||
query = from m in MatchSchema, select: count(m.match_id)
|
|
||||||
LoLAnalytics.Repo.one(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_match(String.t()) :: %LolAnalytics.Match.MatchSchema{}
|
|
||||||
def get_match(match_id) do
|
|
||||||
query = from m in MatchSchema, where: m.match_id == ^match_id
|
|
||||||
|
|
||||||
LoLAnalytics.Repo.one(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec insert_match(String.t()) :: %LolAnalytics.Match.MatchSchema{}
|
|
||||||
def insert_match(match_id) do
|
|
||||||
MatchSchema.changeset(%MatchSchema{}, %{:match_id => match_id, :processed => false})
|
|
||||||
|> LoLAnalytics.Repo.insert()
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec update_match(%LolAnalytics.Match.MatchSchema{}, term()) ::
|
|
||||||
%LolAnalytics.Match.MatchSchema{}
|
|
||||||
def update_match(match, attrs) do
|
|
||||||
match = MatchSchema.changeset(match, attrs)
|
|
||||||
LoLAnalytics.Repo.update(match)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,19 +0,0 @@
|
|||||||
defmodule LolAnalytics.Match.MatchSchema do
|
|
||||||
use Ecto.Schema
|
|
||||||
import Ecto.Changeset
|
|
||||||
|
|
||||||
schema "match" do
|
|
||||||
field :match_id, :string
|
|
||||||
field :processed, :boolean, default: false
|
|
||||||
field :match_url, :string
|
|
||||||
|
|
||||||
timestamps()
|
|
||||||
end
|
|
||||||
|
|
||||||
def changeset(%__MODULE__{} = match, params \\ %{}) do
|
|
||||||
match
|
|
||||||
|> cast(params, [:match_id, :processed, :match_url])
|
|
||||||
|> validate_required([:match_id, :processed])
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -0,0 +1,34 @@
|
|||||||
|
defmodule LolAnalytics.MatchProcessor.MatchesBroadwayProcessor do
|
||||||
|
alias LolAnalytics.Facts.FactsRunner
|
||||||
|
use Broadway
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
Broadway.start_link(__MODULE__,
|
||||||
|
name: __MODULE__,
|
||||||
|
processors: [default: []],
|
||||||
|
producer: [
|
||||||
|
module: {LolAnalytics.MatchProcessor.MatchesProducer, []},
|
||||||
|
rate_limiting: [
|
||||||
|
interval: 1000,
|
||||||
|
allowed_messages: 40
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Broadway
|
||||||
|
def handle_message(_processor, message, _context) do
|
||||||
|
message.data
|
||||||
|
# build_match_url(message.data.queue_id, message.data.patch_number, message.data.match_id)
|
||||||
|
|> FactsRunner.analyze_match()
|
||||||
|
|
||||||
|
message
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_match_url(queue, patch_id, match_id) do
|
||||||
|
"http://192.168.1.55:9000/#{queue_to_dir(queue)}/#{patch_id}/#{match_id}.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp queue_to_dir(420), do: "ranked"
|
||||||
|
defp queue_to_dir(_), do: "ranked"
|
||||||
|
end
|
@ -0,0 +1,33 @@
|
|||||||
|
defmodule LolAnalytics.MatchProcessor.MatchesProducer do
|
||||||
|
use GenStage
|
||||||
|
|
||||||
|
@impl GenStage
|
||||||
|
def init(opts) do
|
||||||
|
{:producer, opts}
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
GenStage.start_link(__MODULE__, :ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenStage
|
||||||
|
def handle_demand(demand, state) do
|
||||||
|
matches = query_unprocessed_matches(demand)
|
||||||
|
|
||||||
|
{:noreply, matches, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp query_unprocessed_matches(demand) when demand <= 0, do: []
|
||||||
|
|
||||||
|
defp query_unprocessed_matches(demand) do
|
||||||
|
LolAnalytics.Dimensions.Match.MatchRepo.list_unprocessed_matches(demand)
|
||||||
|
|> Enum.map(&broadway_transform/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp broadway_transform(match) do
|
||||||
|
%Broadway.Message{
|
||||||
|
data: match,
|
||||||
|
acknowledger: Broadway.NoopAcknowledger.init()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
@ -42,8 +42,11 @@ defmodule LoLAnalytics.MixProject do
|
|||||||
{:postgrex, ">= 0.0.0"},
|
{:postgrex, ">= 0.0.0"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:lol_api, in_umbrella: true},
|
{:lol_api, in_umbrella: true},
|
||||||
|
{:storage, in_umbrella: true},
|
||||||
{:httpoison, "~> 2.2"},
|
{:httpoison, "~> 2.2"},
|
||||||
{:poison, "~> 5.0"}
|
{:poison, "~> 5.0"},
|
||||||
|
{:gen_stage, "~> 1.2.1"},
|
||||||
|
{:broadway, "~> 1.1"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table("dim_champion") do
|
||||||
|
add :champion_id, :integer, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_champion", [:champion_id], unique: true)
|
||||||
|
|
||||||
|
create table("dim_item") do
|
||||||
|
add :item_id, :integer, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_item", [:item_id], unique: true)
|
||||||
|
|
||||||
|
create table("dim_patch") do
|
||||||
|
add :patch_number, :string, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_patch", [:patch_number], unique: true)
|
||||||
|
|
||||||
|
create table("dim_match") do
|
||||||
|
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||||
|
add :match_id, :string, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_match", [:patch_number])
|
||||||
|
create index("dim_match", [:match_id], unique: true)
|
||||||
|
|
||||||
|
create table("dim_player") do
|
||||||
|
add :puuid, :string, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_player", [:puuid], unique: true)
|
||||||
|
|
||||||
|
create table("dim_summoner_spell") do
|
||||||
|
add :spell_id, :integer, primary_key: true, null: false
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_summoner_spell", [:spell_id], unique: true)
|
||||||
|
|
||||||
|
create table("fact_champion_played_game") do
|
||||||
|
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
|
||||||
|
add :match_id, references("dim_match", column: :match_id, type: :string)
|
||||||
|
add :is_win, :boolean
|
||||||
|
add :game_length_seconds, :integer
|
||||||
|
add :queue_id, :integer
|
||||||
|
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||||
|
add :team_position, :string
|
||||||
|
add :puuid, references("dim_player", column: :puuid, type: :string)
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("fact_champion_played_game", [:id, :champion_id, :queue_id])
|
||||||
|
create index("fact_champion_played_game", [:puuid, :match_id], unique: true)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,10 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.ChampionDimName do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("dim_champion") do
|
||||||
|
add :name, :string
|
||||||
|
add :image, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,31 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellWinRate do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table("fact_champion_picked_summoner_spell") do
|
||||||
|
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
|
||||||
|
add :summoner_spell_id, references("dim_summoner_spell", column: :spell_id, type: :integer)
|
||||||
|
add :match_id, references("dim_match", column: :match_id, type: :string)
|
||||||
|
add :is_win, :boolean
|
||||||
|
add :game_length_seconds, :integer
|
||||||
|
add :queue_id, :integer
|
||||||
|
add :team_position, :string
|
||||||
|
add :puuid, references("dim_player", column: :puuid, type: :string)
|
||||||
|
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(
|
||||||
|
"fact_champion_picked_summoner_spell",
|
||||||
|
[:puuid, :match_id, :summoner_spell_id],
|
||||||
|
unique: true
|
||||||
|
)
|
||||||
|
|
||||||
|
create index("fact_champion_picked_summoner_spell", [
|
||||||
|
:is_win,
|
||||||
|
:team_position,
|
||||||
|
:queue_id,
|
||||||
|
:summoner_spell_id,
|
||||||
|
:patch_number
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadata do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("dim_summoner_spell") do
|
||||||
|
add :metadata, :map
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadataIndex do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("CREATE INDEX dim_summoner_spell_metadata ON dim_summoner_spell USING GIN(metadata)")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("DROP INDEX dim_summoner_spell_metadata")
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.ChampionPickedItem do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table("fact_champion_picked_item") do
|
||||||
|
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
|
||||||
|
add :item_id, references("dim_item", column: :item_id, type: :integer)
|
||||||
|
add :slot_number, :integer
|
||||||
|
add :match_id, references("dim_match", column: :match_id, type: :string)
|
||||||
|
add :is_win, :boolean
|
||||||
|
add :game_length_seconds, :integer
|
||||||
|
add :queue_id, :integer
|
||||||
|
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||||
|
add :team_position, :string
|
||||||
|
add :puuid, references("dim_player", column: :puuid, type: :string)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("fact_champion_picked_item", [:champion_id])
|
||||||
|
create index("fact_champion_picked_item", [:puuid, :match_id, :slot_number, :item_id], unique: true)
|
||||||
|
create index("fact_champion_picked_item", [
|
||||||
|
:item_id,
|
||||||
|
:queue_id,
|
||||||
|
:is_win,
|
||||||
|
:patch_number,
|
||||||
|
:team_position
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.ItemMetadata do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("dim_item") do
|
||||||
|
add :metadata, :map
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.ItemMetadataIndex do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("CREATE INDEX dim_item_metadata ON dim_item USING GIN(metadata)")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("DROP INDEX dim_item_metadata")
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,15 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.DimMatchFactProcessingStatus do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("dim_match") do
|
||||||
|
add :fact_champion_played_game_status, :integer, default: 0
|
||||||
|
add :fact_champion_picked_item_status, :integer, default: 0
|
||||||
|
add :fact_champion_picked_summoner_spell_status, :integer, default: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
create index("dim_match", [:fact_champion_played_game_status])
|
||||||
|
create index("dim_match", [:fact_champion_picked_item_status])
|
||||||
|
create index("dim_match", [:fact_champion_picked_summoner_spell_status])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.DimMatchQueueId do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table "dim_match" do
|
||||||
|
add :queue_id, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
defmodule LoLAnalytics.Repo.Migrations.DimPlayerLastProcessed do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table "dim_player" do
|
||||||
|
add :last_processed_at, :utc_datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,3 +3,16 @@
|
|||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
/* This file is for your main application CSS */
|
/* This file is for your main application CSS */
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-tooltip:hover .tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
z-index: 100;
|
||||||
|
border: 1px solid #333;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
@ -18,18 +18,22 @@
|
|||||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||||
import "phoenix_html"
|
import "phoenix_html"
|
||||||
// Establish Phoenix Socket and LiveView configuration.
|
// Establish Phoenix Socket and LiveView configuration.
|
||||||
import {Socket} from "phoenix"
|
import { Socket } from "phoenix"
|
||||||
import {LiveSocket} from "phoenix_live_view"
|
import { LiveSocket } from "phoenix_live_view"
|
||||||
import topbar from "../vendor/topbar"
|
import topbar from "../vendor/topbar"
|
||||||
|
import { ChampionWinRate } from "./hooks/champion_win_rate_patch"
|
||||||
|
|
||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||||
let liveSocket = new LiveSocket("/live", Socket, {
|
let liveSocket = new LiveSocket("/live", Socket, {
|
||||||
longPollFallbackMs: 2500,
|
longPollFallbackMs: 2500,
|
||||||
params: {_csrf_token: csrfToken}
|
params: { _csrf_token: csrfToken },
|
||||||
|
hooks: {
|
||||||
|
ChampionWinRate,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show progress bar on live navigation and form submits
|
// Show progress bar on live navigation and form submits
|
||||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
|
||||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||||
|
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import Chart from "chart.js/auto"
|
||||||
|
|
||||||
|
const ChampionWinRate = {
|
||||||
|
mounted() {
|
||||||
|
this.handleEvent("win-rate", ({ winRates }) => {
|
||||||
|
this.sortedWinRates = winRates.sort((a, b) => {
|
||||||
|
let [p1Major, p1Minor] = a.patch_number.split(".").map(Number);
|
||||||
|
let [p2Major, p2Minor] = b.patch_number.split(".").map(Number);
|
||||||
|
|
||||||
|
|
||||||
|
if (p1Major > p2Major || (p1Major == p2Major && p1Minor > p2Minor)) return 1;
|
||||||
|
return -1
|
||||||
|
})
|
||||||
|
this.patches = this.sortedWinRates.map((winRate) => {
|
||||||
|
return winRate.patch_number
|
||||||
|
})
|
||||||
|
this.winRateValues = this.sortedWinRates.map((winRate) => winRate.win_rate)
|
||||||
|
// TODO: it breaks on liveview updates, should apply a better fix...
|
||||||
|
setInterval(() => {
|
||||||
|
const data = {
|
||||||
|
labels: this.patches,
|
||||||
|
datasets: [{
|
||||||
|
data: this.winRateValues,
|
||||||
|
fill: false,
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
tension: 0.1
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
this.chart = new Chart(document.getElementById("win-rate"), {
|
||||||
|
type: 'line',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.chart.canvas.parentNode.style.height = '250px';
|
||||||
|
this.chart.canvas.parentNode.style.width = '400px';
|
||||||
|
this.chart.labels.display = false;
|
||||||
|
this.chart.options.legend.display = false
|
||||||
|
this.chart.options.legend.display = false
|
||||||
|
}, 1000)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChampionWinRate };
|
28
apps/lol_analytics_web/assets/package-lock.json
generated
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"chart.js": "^4.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||||
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
apps/lol_analytics_web/assets/package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"chart.js": "^4.4.3"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
attr :id, :integer, required: true
|
||||||
|
attr :image, :string, required: true
|
||||||
|
attr :name, :string, required: true
|
||||||
|
attr :width, :integer, default: 100
|
||||||
|
attr :height, :integer, default: 100
|
||||||
|
|
||||||
|
def champion_avatar(assigns) do
|
||||||
|
~H"""
|
||||||
|
<style>
|
||||||
|
.champion-avatar {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="flex flex-col w-40">
|
||||||
|
<img src={@image} class="champion-avatar rounded-xl" alt="champion-icon" />
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,39 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
|
||||||
|
defstruct [:id, :win_rate, :image, :name, :team_position, :patch_number, :wins, :total_games]
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias LolAnalyticsWeb.ChampionComponents.ChampionCard.Props
|
||||||
|
|
||||||
|
attr :props, Props, default: %Props{}
|
||||||
|
|
||||||
|
def champion_card(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.link patch={"/champions/#{@props.id}?team-position=#{@props.team_position}&patch=#{@props.patch_number}"}>
|
||||||
|
<div class="flex flex-col rounded-xl bg-clip-border overflow-hidden bg-gray-200">
|
||||||
|
<div class="flex flex-col flex-col-reverse">
|
||||||
|
<div class="flex w-auto px-4 py-1 opacity-80 gap-2 absolute z-10 align-bottom bg-black">
|
||||||
|
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
|
||||||
|
<h3 class="text-white"><%= @props.name %></h3>
|
||||||
|
</div>
|
||||||
|
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"} />
|
||||||
|
</div>
|
||||||
|
<div class="py-1" />
|
||||||
|
<div class="pl-2">
|
||||||
|
<h3>Win rate: <%= @props.win_rate %></h3>
|
||||||
|
<p class="text-xs">Wins: <%= @props.wins %> / <%= @props.total_games %></p>
|
||||||
|
</div>
|
||||||
|
<div class="py-1" />
|
||||||
|
</div>
|
||||||
|
</.link>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
|
||||||
|
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
|
||||||
|
defp team_position_image("TOP"), do: "/images/lanes/top.png"
|
||||||
|
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
|
||||||
|
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
|
||||||
|
end
|
@ -0,0 +1,23 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.Items do
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
def items(assigns) do
|
||||||
|
image = ""
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-wrap flex-wrap gap-4">
|
||||||
|
<%= for item <- assigns.items do %>
|
||||||
|
<div class="has-tooltip">
|
||||||
|
<div clas="flex flex-col gap-1 p-4">
|
||||||
|
<img class="rounded-xl" src={item.image} />
|
||||||
|
<%!-- <p><%= item.name %></p> --%>
|
||||||
|
<p><%= item.win_rate %>%</p>
|
||||||
|
<p class="text-xs"><%= item.wins %>/<%= item.total_games %></p>
|
||||||
|
</div>
|
||||||
|
<span class="tooltip -mt-8 py-2 px-4 rounded-xl"><%= item.name %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,23 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells do
|
||||||
|
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
def summoner_spells(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-wrap flex-wrap gap-4">
|
||||||
|
<%= for spell <- assigns.spells do %>
|
||||||
|
<div class="has-tooltip">
|
||||||
|
<div clas="flex flex-col gap-1">
|
||||||
|
<img class="rounded-xl" src={spell.image} />
|
||||||
|
<p><%= spell.win_rate %>%</p>
|
||||||
|
<p class="text-xs"><%= spell.wins %>/<%= spell.total_games %></p>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip -my-8 px-4 py-2 rounded-xl">
|
||||||
|
<p><%= spell.name %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -315,8 +315,8 @@ defmodule LoLAnalyticsWeb.CoreComponents do
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={@id}
|
id={@id}
|
||||||
name={@name}
|
name={@name}
|
||||||
value="true"
|
value="false"
|
||||||
checked={@checked}
|
checked={false}
|
||||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
|
@ -2,25 +2,13 @@
|
|||||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src={~p"/images/logo.svg"} width="36" />
|
LoL Analytics
|
||||||
</a>
|
</a>
|
||||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
|
||||||
v<%= Application.spec(:phoenix, :vsn) %>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
||||||
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
|
<.link patch="/champions">
|
||||||
@elixirphoenix
|
Champions
|
||||||
</a>
|
</.link>
|
||||||
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://hexdocs.pm/phoenix/overview.html"
|
|
||||||
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
|
|
||||||
>
|
|
||||||
Get Started <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="[scrollbar-gutter:stable]">
|
<html lang="en" class="[scrollbar-gutter:stable]">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="csrf-token" content={get_csrf_token()} />
|
<meta name="csrf-token" content={get_csrf_token()} />
|
||||||
@ -10,8 +11,10 @@
|
|||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white antialiased">
|
|
||||||
|
<body class="bg-white antialiased">
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -0,0 +1,28 @@
|
|||||||
|
defmodule LolAnalyticsWeb.Loader do
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
def loader(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div role="status" class="flex flex-col mx-auto w-max justify-center items-center gap-4">
|
||||||
|
<span class="text-3xl">Loading</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="w-16 h-16 text-gray-300 animate-spin dark:text-gray-300 fill-orange-600"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,66 @@
|
|||||||
|
defmodule LolAnalyticsWeb.PatchSelector do
|
||||||
|
use LoLAnalyticsWeb, :live_component
|
||||||
|
|
||||||
|
def mount(socket) do
|
||||||
|
patches =
|
||||||
|
LolAnalytics.Dimensions.Patch.PatchRepo.list_patches()
|
||||||
|
|> Enum.map(fn patch ->
|
||||||
|
%{patch_number: String.split(patch.patch_number, ".") |> Enum.take(2) |> Enum.join(".")}
|
||||||
|
end)
|
||||||
|
|> MapSet.new()
|
||||||
|
|> Enum.to_list()
|
||||||
|
|> Enum.sort(fn %{patch_number: p1}, %{patch_number: p2} ->
|
||||||
|
[major_1, minor_1] = String.split(p1, ".") |> Enum.map(&String.to_integer/1)
|
||||||
|
[major_2, minor_2] = String.split(p2, ".") |> Enum.map(&String.to_integer/1)
|
||||||
|
|
||||||
|
major_1 > major_2 || (major_1 == major_2 && minor_1 > minor_2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
patch_numbers = Enum.map(patches, & &1.patch_number)
|
||||||
|
[last_patch | _] = patch_numbers
|
||||||
|
|
||||||
|
send(self(), %{patch: last_patch})
|
||||||
|
|
||||||
|
socket =
|
||||||
|
assign(socket, :patch_numbers, patch_numbers)
|
||||||
|
|> assign(:patch_form, to_form(%{"selected_patch" => last_patch}))
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("selected_patch", %{"patch" => patch} = unsigned_params, socket) do
|
||||||
|
send(self(), %{patch: patch})
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :selected_patch, patch)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div phx-feedback-for={@id}>
|
||||||
|
<style>
|
||||||
|
.patch-selector {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<.form for={@patch_form} phx-change="validate" phx-target={@myself} phx-submit="save">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<p class="my-auto">Patch</p>
|
||||||
|
<select
|
||||||
|
phx-change="selected_patch"
|
||||||
|
id="patch"
|
||||||
|
name="patch"
|
||||||
|
class="patch-selector cursor-pointer block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
|
||||||
|
>
|
||||||
|
<%= for patch <- @patch_numbers do %>
|
||||||
|
<option key={patch} phx-click="select-patch" name={patch} value={patch}>
|
||||||
|
<%= patch %>
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,3 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionLive.ChampionSummary do
|
||||||
|
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
|
||||||
|
end
|
@ -0,0 +1,56 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
|
||||||
|
use LoLAnalyticsWeb, :live_component
|
||||||
|
|
||||||
|
@roles_definition [
|
||||||
|
%{title: "All", value: "all"},
|
||||||
|
%{title: "Top", value: "TOP"},
|
||||||
|
%{title: "Jungle", value: "JUNGLE"},
|
||||||
|
%{title: "Mid", value: "MIDDLE"},
|
||||||
|
%{title: "Bot", value: "BOTTOM"},
|
||||||
|
%{title: "Support", value: "UTILITY"}
|
||||||
|
]
|
||||||
|
|
||||||
|
def on_role_selected(role) do
|
||||||
|
send_update(LolAnalyticsWeb.ChampionLive, role)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mount(socket) do
|
||||||
|
socket =
|
||||||
|
assign(socket, :roles, @roles_definition)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :selectedrole, :string, required: true
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
selected_class =
|
||||||
|
"px-8 py-2 flex flex-row gap-2 align-middle rounded-full border-gray-200 border border-orange-600 cursor-pointer"
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row overflow-x-scroll sm:overflow-x-auto justify-between">
|
||||||
|
<%= for role <- @roles do %>
|
||||||
|
<%= if (assigns.selectedrole == role.value) do %>
|
||||||
|
<div phx-click="filter" phx-value-role={role.title} class={selected_class}>
|
||||||
|
<p><%= role.title %></p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div
|
||||||
|
phx-click="filter"
|
||||||
|
phx-value-role={role.value}
|
||||||
|
class="px-8 py-2 flex flex-row gap-2 align-middle cursor-pointer"
|
||||||
|
>
|
||||||
|
<p><%= role.title %></p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters.EventHandler do
|
||||||
|
@callback role_selected() :: none()
|
||||||
|
end
|
@ -0,0 +1,79 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionsList do
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
attr :champion, :map
|
||||||
|
|
||||||
|
defp render_champion(assigns) do
|
||||||
|
detail_url =
|
||||||
|
"/champions/#{assigns.champion.id}?team-position=#{assigns.champion.team_position}&patch=#{assigns.champion.patch_number}"
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<.link patch={detail_url}>
|
||||||
|
<div class="flex cursor-pointer items-center gap-2">
|
||||||
|
<img
|
||||||
|
class="champion_image rounded-md"
|
||||||
|
src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{assigns.champion.image}"}
|
||||||
|
/>
|
||||||
|
<%= @champion.name %>
|
||||||
|
</div>
|
||||||
|
</.link>
|
||||||
|
</td>
|
||||||
|
<td><%= @champion.win_rate %>%</td>
|
||||||
|
<td><%= @champion.total_games %></td>
|
||||||
|
<td>
|
||||||
|
<img src={team_position_image(@champion.team_position)} class="w-5 h-5" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :champions, :list
|
||||||
|
attr :patch_number, :integer
|
||||||
|
attr :position, :string
|
||||||
|
|
||||||
|
def champions_list(assigns) do
|
||||||
|
~H"""
|
||||||
|
<style>
|
||||||
|
.champion_image {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||||
|
Champion
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||||
|
Win rate
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||||
|
Total games
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||||
|
Position
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<%= for champion <- assigns.champions do %>
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
<.link patch={"/champions/#{champion.id}?team-position=#{champion.team_position}&patch=#{champion.patch_number}"}>
|
||||||
|
<.render_champion champion={champion} ) />
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<!-- table rows and cells go here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
|
||||||
|
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
|
||||||
|
defp team_position_image("TOP"), do: "/images/lanes/top.png"
|
||||||
|
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
|
||||||
|
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
|
||||||
|
end
|
@ -0,0 +1,198 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.ChampionLive.Index do
|
||||||
|
use LoLAnalyticsWeb, :live_view
|
||||||
|
|
||||||
|
import LolAnalyticsWeb.ChampionComponents.ChampionCard
|
||||||
|
import LolAnalyticsWeb.ChampionLive.Components.ChampionsList
|
||||||
|
import LolAnalyticsWeb.Loader
|
||||||
|
import Phoenix.VerifiedRoutes
|
||||||
|
|
||||||
|
alias LolAnalyticsWeb.ChampionLive.Mapper
|
||||||
|
alias LolAnalyticsWeb.ChampionLive.Components.ChampionFilters
|
||||||
|
alias LolAnalyticsWeb.PatchSelector
|
||||||
|
|
||||||
|
@behaviour LolAnalyticsWeb.ChampionFilters.EventHandler
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(params, _session, socket) do
|
||||||
|
role = params["role"] || "all"
|
||||||
|
patch = params["patch"] || "14.12"
|
||||||
|
display_mode = params["display_mode"] || "grid"
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:selected_role, role)
|
||||||
|
|> assign(:selected_patch, patch)
|
||||||
|
|> assign(:champions, %{status: :loading})
|
||||||
|
|> assign(:display_mode, display_mode)
|
||||||
|
|> load_champs(role, patch)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_params(params, _uri, socket) do
|
||||||
|
role = params["role"] || "all"
|
||||||
|
patch = params["patch"] || "14.10"
|
||||||
|
display_mode = params["display_mode"] || "grid"
|
||||||
|
|
||||||
|
{
|
||||||
|
:noreply,
|
||||||
|
assign(socket, selected_role: role)
|
||||||
|
|> assign(selected_patch: patch)
|
||||||
|
|> assign(display_mode: display_mode)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("filter", %{"role" => selected_role} = params, socket) do
|
||||||
|
query =
|
||||||
|
get_query_params(socket)
|
||||||
|
|> Map.merge(%{role: selected_role})
|
||||||
|
|
||||||
|
{:reply, %{},
|
||||||
|
socket
|
||||||
|
|> push_patch(to: ~p"/champions?#{query}")
|
||||||
|
|> assign(:champions, %{status: :loading})
|
||||||
|
|> load_champs(selected_role, socket.assigns.selected_patch)
|
||||||
|
|> assign(:selected_role, selected_role)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("set-display-mode", %{"mode" => mode} = params, socket) do
|
||||||
|
query_params =
|
||||||
|
get_query_params(socket)
|
||||||
|
|> Map.merge(%{display_mode: mode})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
assign(socket, :display_mode, mode)
|
||||||
|
|> push_patch(to: ~p"/champions?#{query_params}")}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(%{patch: patch}, socket) do
|
||||||
|
selected_role = socket.assigns.selected_role
|
||||||
|
|
||||||
|
query_params =
|
||||||
|
get_query_params(socket)
|
||||||
|
|> Map.merge(%{patch: patch})
|
||||||
|
|
||||||
|
socket =
|
||||||
|
assign(socket, :champions, %{status: :loading})
|
||||||
|
|> assign(:selected_patch, patch)
|
||||||
|
|> push_patch(to: ~p"/champions?#{query_params}")
|
||||||
|
|> load_champs(selected_role, patch)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_champs(socket, selected_role, "all") do
|
||||||
|
socket
|
||||||
|
|> start_async(
|
||||||
|
:get_champs,
|
||||||
|
fn ->
|
||||||
|
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(team_position: selected_role)
|
||||||
|
|> Mapper.map_champs()
|
||||||
|
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_champs(socket, selected_role, patch) do
|
||||||
|
socket
|
||||||
|
|> start_async(
|
||||||
|
:get_champs,
|
||||||
|
fn ->
|
||||||
|
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(
|
||||||
|
team_position: selected_role,
|
||||||
|
patch_number: patch
|
||||||
|
)
|
||||||
|
|> Mapper.map_champs()
|
||||||
|
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:get_champs, {:ok, champs}, socket) do
|
||||||
|
{:noreply, assign(socket, :champions, %{status: :data, data: champs})}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_display_mode_selector_selector(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex">
|
||||||
|
<div phx-click="set-display-mode" phx-value-mode="grid" class="cursor-pointer">
|
||||||
|
<.icon name={grid_icon(assigns.display_mode)} alt="table" />
|
||||||
|
</div>
|
||||||
|
<div phx-click="set-display-mode" phx-value-mode="list" class="cursor-pointer">
|
||||||
|
<.icon name={list_icon(assigns.display_mode)} alt="table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp grid_icon(selected_display_mode) do
|
||||||
|
case selected_display_mode do
|
||||||
|
"grid" -> "hero-squares-2x2-solid"
|
||||||
|
"list" -> "hero-squares-2x2"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_icon(selected_display_mode) do
|
||||||
|
case selected_display_mode do
|
||||||
|
"grid" -> "hero-table-cells"
|
||||||
|
"list" -> "hero-table-cells-solid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_champions_list(assigns) do
|
||||||
|
case assigns.champions do
|
||||||
|
%{status: :loading} ->
|
||||||
|
~H"""
|
||||||
|
<.loader />
|
||||||
|
"""
|
||||||
|
|
||||||
|
%{status: :data, data: champions} ->
|
||||||
|
~H"""
|
||||||
|
<.champions_list champions={champions} />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_champions_grid(%{:champions => champions_state} = assigns) do
|
||||||
|
case champions_state do
|
||||||
|
%{status: :loading} ->
|
||||||
|
~H"""
|
||||||
|
<.loader />
|
||||||
|
"""
|
||||||
|
|
||||||
|
%{status: :data, data: champions} ->
|
||||||
|
~H"""
|
||||||
|
<div id="champions" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<%= for champion <- champions do %>
|
||||||
|
<.champion_card props={champion} />
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_role(role) do
|
||||||
|
fn champ ->
|
||||||
|
champ.team_position == role || role == "all"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(params, _url, socket) do
|
||||||
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :index, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Listing Champions")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_query_params(socket) do
|
||||||
|
%{
|
||||||
|
patch: socket.assigns.selected_patch,
|
||||||
|
role: socket.assigns.selected_role,
|
||||||
|
display_mode: socket.assigns.display_mode
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,32 @@
|
|||||||
|
<.header>
|
||||||
|
<p class="text-3xl">
|
||||||
|
Champions
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<div class="h-4" />
|
||||||
|
|
||||||
|
<div class="px-2">
|
||||||
|
<h1 class="text-l font-semibold">Filters</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col py-2 gap-2">
|
||||||
|
<p>Roles</p>
|
||||||
|
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role || "all" } />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<.live_component module={PatchSelector} id="patch-selector" initial_patch={@selected_patch} />
|
||||||
|
|
||||||
|
<.render_display_mode_selector_selector display_mode={@display_mode} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="h-4"></div>
|
||||||
|
|
||||||
|
<%= if @display_mode=="grid" do %>
|
||||||
|
<.render_champions_grid champions={@champions} />
|
||||||
|
<% else %>
|
||||||
|
<.render_champions_list id="champions-list" champions={@champions} />
|
||||||
|
<% end %>
|
||||||
|
</div>
|
@ -0,0 +1,13 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionLive.Mapper do
|
||||||
|
alias LolAnalyticsWeb.ChampionLive.ChampionSummary
|
||||||
|
|
||||||
|
def map_champs(champs) do
|
||||||
|
champs
|
||||||
|
|> Enum.map(fn champ ->
|
||||||
|
%{
|
||||||
|
champ
|
||||||
|
| win_rate: :erlang.float_to_binary(champ.win_rate, decimals: 2)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,167 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.ChampionLive.Show do
|
||||||
|
use LoLAnalyticsWeb, :live_view
|
||||||
|
|
||||||
|
import LolAnalyticsWeb.ChampionComponents.SummonerSpells
|
||||||
|
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
|
||||||
|
import LolAnalyticsWeb.ChampionComponents.Items
|
||||||
|
import LolAnalyticsWeb.Loader
|
||||||
|
|
||||||
|
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(%{"id" => id, "team-position" => team_position, "patch" => patch}, _, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||||
|
|> assign(:champion, load_champion_info(id))
|
||||||
|
|> load_win_rates(id, team_position)
|
||||||
|
|> load_summoner_spells(id, team_position, patch)
|
||||||
|
|> load_items(id, team_position, patch)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_win_rates(socket, champion_id, team_position) do
|
||||||
|
socket
|
||||||
|
|> start_async(:get_win_rates, fn ->
|
||||||
|
LolAnalytics.Facts.ChampionPlayedGame.Repo.champion_win_rates_by_patch(
|
||||||
|
champion_id,
|
||||||
|
team_position
|
||||||
|
)
|
||||||
|
|> Enum.sort(fn p1, p2 ->
|
||||||
|
[_, minor_1] = String.split(p1.patch_number, ".") |> Enum.map(&String.to_integer/1)
|
||||||
|
[_, minor_2] = String.split(p2.patch_number, ".") |> Enum.map(&String.to_integer/1)
|
||||||
|
|
||||||
|
p1 < p2 && minor_1 < minor_2
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_summoner_spells(socket, champion_id, team_position, patch_number) do
|
||||||
|
socket
|
||||||
|
|> assign(:summoner_spells, %{status: :loading})
|
||||||
|
|> start_async(:get_summoners, fn ->
|
||||||
|
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(
|
||||||
|
champion_id,
|
||||||
|
team_position,
|
||||||
|
patch_number
|
||||||
|
)
|
||||||
|
|> ShowMapper.map_spells()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_items(socket, champion_id, team_position, patch) do
|
||||||
|
socket
|
||||||
|
|> assign(:items, %{status: :loading})
|
||||||
|
|> assign(:boots, %{status: :loading})
|
||||||
|
|> start_async(
|
||||||
|
:get_items,
|
||||||
|
fn ->
|
||||||
|
items =
|
||||||
|
LolAnalytics.Facts.ChampionPickedItem.Repo.get_champion_picked_items(
|
||||||
|
champion_id,
|
||||||
|
team_position,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
|
||||||
|
popular_items = items |> ShowMapper.map_items() |> Enum.take(30)
|
||||||
|
|
||||||
|
boots = items |> ShowMapper.extract_boots()
|
||||||
|
|
||||||
|
%{boots: boots, popular: popular_items}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
@spec handle_async(:get_items | :get_summoners | :get_win_rates, {:ok, any()}, map()) ::
|
||||||
|
{:noreply, map()}
|
||||||
|
def handle_async(:get_win_rates, {:ok, result}, socket) do
|
||||||
|
{:noreply, push_event(socket, "win-rate", %{winRates: result})}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:get_items, {:ok, %{popular: popular, boots: boots}} = result, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:items, %{
|
||||||
|
status: :data,
|
||||||
|
data: %{
|
||||||
|
popular: popular,
|
||||||
|
boots: boots
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:get_summoners, {:ok, summoner_spells}, socket) do
|
||||||
|
socket =
|
||||||
|
assign(socket, :summoner_spells, %{
|
||||||
|
status: :data,
|
||||||
|
data: summoner_spells
|
||||||
|
})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_champion_info(champion_id) do
|
||||||
|
LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
|
||||||
|
|> ShowMapper.map_champion()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp page_title(:show), do: "Show Champion"
|
||||||
|
defp page_title(:edit), do: "Edit Champion"
|
||||||
|
|
||||||
|
def render_items(assigns) do
|
||||||
|
case assigns.items do
|
||||||
|
%{status: :loading} ->
|
||||||
|
~H"""
|
||||||
|
<.loader />
|
||||||
|
"""
|
||||||
|
|
||||||
|
%{status: :data, data: data} ->
|
||||||
|
~H"""
|
||||||
|
<%= if Enum.count(data.boots) > 0 do %>
|
||||||
|
<h2 class="text-2xl">Items</h2>
|
||||||
|
|
||||||
|
<h2 class="text-xl">Boots</h2>
|
||||||
|
|
||||||
|
<div class="my-2" />
|
||||||
|
|
||||||
|
<.items items={data.boots} />
|
||||||
|
|
||||||
|
<div class="my-4" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h2 class="text-xl">Popular items</h2>
|
||||||
|
|
||||||
|
<div class="my-2" />
|
||||||
|
|
||||||
|
<.items items={data.popular} />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_summoners(assigns) do
|
||||||
|
case assigns.summoner_pells do
|
||||||
|
%{status: :loading} ->
|
||||||
|
~H"""
|
||||||
|
<h2 class="text-2xl">Summoner spells</h2>
|
||||||
|
<div class="my-2" />
|
||||||
|
<.loader />
|
||||||
|
"""
|
||||||
|
|
||||||
|
%{status: :data, data: data} ->
|
||||||
|
~H"""
|
||||||
|
<h2 class="text-2xl">Summoner spells</h2>
|
||||||
|
|
||||||
|
<div class="my-2" />
|
||||||
|
|
||||||
|
<.summoner_spells spells={data} />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,36 @@
|
|||||||
|
<style>
|
||||||
|
.win-rate {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win-rate-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<.header>
|
||||||
|
<h1 class="text-4xl">
|
||||||
|
<%= @champion.name %>
|
||||||
|
</h1>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<div class="my-6" />
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<.champion_avatar class="w-20" id={@champion.id} name={@champion.name}
|
||||||
|
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
|
||||||
|
<div>
|
||||||
|
<p class="text-lg">Win rate by patch</p>
|
||||||
|
<div class="win-rate-container" style="position: relative; height:250px; width:400px">
|
||||||
|
<canvas class="w-full win-rate" height="250" class="win-rate" id="win-rate" phx-hook="ChampionWinRate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4" />
|
||||||
|
|
||||||
|
<.render_summoners summoner_pells={@summoner_spells} />
|
||||||
|
|
||||||
|
<div class="my-4" />
|
||||||
|
|
||||||
|
<.render_items items={@items} />
|
@ -0,0 +1,60 @@
|
|||||||
|
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
|
||||||
|
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell
|
||||||
|
|
||||||
|
@spec map_champion(%LolAnalytics.Dimensions.Champion.ChampionSchema{}) :: map()
|
||||||
|
def map_champion(champion) do
|
||||||
|
%{
|
||||||
|
id: champion.champion_id,
|
||||||
|
name: champion.name,
|
||||||
|
image: champion.image
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_spells(items) do
|
||||||
|
items
|
||||||
|
|> Enum.map(&map_spell/1)
|
||||||
|
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_spell(spell) do
|
||||||
|
image = spell.metadata["image"]["full"]
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: spell["id"],
|
||||||
|
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
|
||||||
|
total_games: spell.total_games,
|
||||||
|
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
|
||||||
|
name: spell.metadata["name"],
|
||||||
|
wins: spell.wins
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_items(items) do
|
||||||
|
items
|
||||||
|
|> Enum.map(&map_item/1)
|
||||||
|
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_item(item) do
|
||||||
|
image = item.metadata["image"]["full"]
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: item["id"],
|
||||||
|
win_rate: :erlang.float_to_binary(item.win_rate, decimals: 2),
|
||||||
|
total_games: item.total_games,
|
||||||
|
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/item/#{image}",
|
||||||
|
name: item.metadata["name"],
|
||||||
|
wins: item.wins
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_boots(items) do
|
||||||
|
items
|
||||||
|
|> Enum.filter(fn item ->
|
||||||
|
tags = item.metadata["tags"]
|
||||||
|
tags |> Enum.any?(&String.equivalent?(&1, "Boots"))
|
||||||
|
end)
|
||||||
|
|> Enum.map(&map_item/1)
|
||||||
|
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.RoleLive.FormComponent do
|
||||||
|
use LoLAnalyticsWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.header>
|
||||||
|
<%= @title %>
|
||||||
|
<:subtitle>Use this form to manage role records in your database.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.simple_form
|
||||||
|
for={@form}
|
||||||
|
id="role-form"
|
||||||
|
phx-target={@myself}
|
||||||
|
phx-change="validate"
|
||||||
|
phx-submit="save"
|
||||||
|
>
|
||||||
|
<:actions>
|
||||||
|
<.button phx-disable-with="Saving...">Save Role</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,34 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.RoleLive.Index do
|
||||||
|
use LoLAnalyticsWeb, :live_view
|
||||||
|
|
||||||
|
@roles ["ALL", "TOP", "MIDDLE", "JUNGLE", "UTILITY", "BOTTOM"]
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, stream(socket, :roles, @roles)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(params, _url, socket) do
|
||||||
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :index, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Listing Roles")
|
||||||
|
|> assign(:role, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({LoLAnalyticsWeb.RoleLive.FormComponent, {:saved, role}}, socket) do
|
||||||
|
{:noreply, stream_insert(socket, :roles, role)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @impl true
|
||||||
|
# def handle_event("delete", %{"id" => id}, socket) do
|
||||||
|
# role = Accounts.get_role!(id)
|
||||||
|
# {:ok, _} = Accounts.delete_role(role)
|
||||||
|
|
||||||
|
# {:noreply, stream_delete(socket, :roles, role)}
|
||||||
|
# end
|
||||||
|
end
|
@ -0,0 +1,4 @@
|
|||||||
|
<.header>
|
||||||
|
Listing Roles
|
||||||
|
<:actions></:actions>
|
||||||
|
</.header>
|
@ -0,0 +1,18 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.RoleLive.Show do
|
||||||
|
use LoLAnalyticsWeb, :live_view
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_params(%{"id" => _id}, _, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, page_title(socket.assigns.live_action))}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp page_title(:show), do: "Show Role"
|
||||||
|
defp page_title(:edit), do: "Edit Role"
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
<.header>
|
||||||
|
Role <%= @role.id %>
|
||||||
|
<:subtitle>This is a role record from your database.</:subtitle>
|
||||||
|
<:actions></:actions>
|
||||||
|
</.header>
|
@ -17,7 +17,13 @@ defmodule LoLAnalyticsWeb.Router do
|
|||||||
scope "/", LoLAnalyticsWeb do
|
scope "/", LoLAnalyticsWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
live "/", ChampionLive.Index, :index
|
||||||
|
live "/champions", ChampionLive.Index, :index
|
||||||
|
live "/champions/new", ChampionLive.Index, :new
|
||||||
|
live "/champions/:id/edit", ChampionLive.Index, :edit
|
||||||
|
|
||||||
|
live "/champions/:id", ChampionLive.Show, :show
|
||||||
|
live "/champions/:id/show/edit", ChampionLive.Show, :edit
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
After Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 6.6 KiB |
BIN
apps/lol_analytics_web/priv/static/images/lanes/bot.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 12 KiB |
BIN
apps/lol_analytics_web/priv/static/images/lanes/jungle.png
Normal file
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/lol_analytics_web/priv/static/images/lanes/mid.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 6.6 KiB |
BIN
apps/lol_analytics_web/priv/static/images/lanes/top.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 7.9 KiB |
BIN
apps/lol_analytics_web/priv/static/images/lanes/utility.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||||
|
#
|
||||||
|
# To ban all spiders from the entire site uncomment the next two lines:
|
||||||
|
# User-agent: *
|
||||||
|
# Disallow: /
|
BIN
apps/lol_analytics_web/priv/static/robots.txt.gz
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.ChampionLiveTest do
|
||||||
|
use LoLAnalyticsWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import LoLAnalytics.AccountsFixtures
|
||||||
|
|
||||||
|
@create_attrs %{}
|
||||||
|
@update_attrs %{}
|
||||||
|
@invalid_attrs %{}
|
||||||
|
|
||||||
|
defp create_champion(_) do
|
||||||
|
champion = champion_fixture()
|
||||||
|
%{champion: champion}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Index" do
|
||||||
|
setup [:create_champion]
|
||||||
|
|
||||||
|
test "lists all champions", %{conn: conn} do
|
||||||
|
{:ok, _index_live, html} = live(conn, ~p"/champions")
|
||||||
|
|
||||||
|
assert html =~ "Listing Champions"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves new champion", %{conn: conn} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/champions")
|
||||||
|
|
||||||
|
assert index_live |> element("a", "New Champion") |> render_click() =~
|
||||||
|
"New Champion"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/champions/new")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#champion-form", champion: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#champion-form", champion: @create_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/champions")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Champion created successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates champion in listing", %{conn: conn, champion: champion} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/champions")
|
||||||
|
|
||||||
|
assert index_live |> element("#champions-#{champion.id} a", "Edit") |> render_click() =~
|
||||||
|
"Edit Champion"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/champions/#{champion}/edit")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#champion-form", champion: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#champion-form", champion: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/champions")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Champion updated successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes champion in listing", %{conn: conn, champion: champion} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/champions")
|
||||||
|
|
||||||
|
assert index_live |> element("#champions-#{champion.id} a", "Delete") |> render_click()
|
||||||
|
refute has_element?(index_live, "#champions-#{champion.id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Show" do
|
||||||
|
setup [:create_champion]
|
||||||
|
|
||||||
|
test "displays champion", %{conn: conn, champion: champion} do
|
||||||
|
{:ok, _show_live, html} = live(conn, ~p"/champions/#{champion}")
|
||||||
|
|
||||||
|
assert html =~ "Show Champion"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates champion within modal", %{conn: conn, champion: champion} do
|
||||||
|
{:ok, show_live, _html} = live(conn, ~p"/champions/#{champion}")
|
||||||
|
|
||||||
|
assert show_live |> element("a", "Edit") |> render_click() =~
|
||||||
|
"Edit Champion"
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/champions/#{champion}/show/edit")
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#champion-form", champion: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#champion-form", champion: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/champions/#{champion}")
|
||||||
|
|
||||||
|
html = render(show_live)
|
||||||
|
assert html =~ "Champion updated successfully"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,108 @@
|
|||||||
|
defmodule LoLAnalyticsWeb.RoleLiveTest do
|
||||||
|
use LoLAnalyticsWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import LoLAnalytics.AccountsFixtures
|
||||||
|
|
||||||
|
@create_attrs %{}
|
||||||
|
@update_attrs %{}
|
||||||
|
@invalid_attrs %{}
|
||||||
|
|
||||||
|
defp create_role(_) do
|
||||||
|
role = role_fixture()
|
||||||
|
%{role: role}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Index" do
|
||||||
|
setup [:create_role]
|
||||||
|
|
||||||
|
test "lists all roles", %{conn: conn} do
|
||||||
|
{:ok, _index_live, html} = live(conn, ~p"/roles")
|
||||||
|
|
||||||
|
assert html =~ "Listing Roles"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves new role", %{conn: conn} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/roles")
|
||||||
|
|
||||||
|
assert index_live |> element("a", "New Role") |> render_click() =~
|
||||||
|
"New Role"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/roles/new")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#role-form", role: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#role-form", role: @create_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/roles")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Role created successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates role in listing", %{conn: conn, role: role} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/roles")
|
||||||
|
|
||||||
|
assert index_live |> element("#roles-#{role.id} a", "Edit") |> render_click() =~
|
||||||
|
"Edit Role"
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/roles/#{role}/edit")
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#role-form", role: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert index_live
|
||||||
|
|> form("#role-form", role: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(index_live, ~p"/roles")
|
||||||
|
|
||||||
|
html = render(index_live)
|
||||||
|
assert html =~ "Role updated successfully"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes role in listing", %{conn: conn, role: role} do
|
||||||
|
{:ok, index_live, _html} = live(conn, ~p"/roles")
|
||||||
|
|
||||||
|
assert index_live |> element("#roles-#{role.id} a", "Delete") |> render_click()
|
||||||
|
refute has_element?(index_live, "#roles-#{role.id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Show" do
|
||||||
|
setup [:create_role]
|
||||||
|
|
||||||
|
test "displays role", %{conn: conn, role: role} do
|
||||||
|
{:ok, _show_live, html} = live(conn, ~p"/roles/#{role}")
|
||||||
|
|
||||||
|
assert html =~ "Show Role"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates role within modal", %{conn: conn, role: role} do
|
||||||
|
{:ok, show_live, _html} = live(conn, ~p"/roles/#{role}")
|
||||||
|
|
||||||
|
assert show_live |> element("a", "Edit") |> render_click() =~
|
||||||
|
"Edit Role"
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/roles/#{role}/show/edit")
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#role-form", role: @invalid_attrs)
|
||||||
|
|> render_change() =~ "can't be blank"
|
||||||
|
|
||||||
|
assert show_live
|
||||||
|
|> form("#role-form", role: @update_attrs)
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert_patch(show_live, ~p"/roles/#{role}")
|
||||||
|
|
||||||
|
html = render(show_live)
|
||||||
|
assert html =~ "Role updated successfully"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -18,7 +18,7 @@ defmodule LoLAPI.AccountApi do
|
|||||||
200 ->
|
200 ->
|
||||||
{:ok, Poison.decode(response.body)}
|
{:ok, Poison.decode(response.body)}
|
||||||
|
|
||||||
code ->
|
_code ->
|
||||||
Logger.error("Error getting puuid from player #{name} \##{tag}")
|
Logger.error("Error getting puuid from player #{name} \##{tag}")
|
||||||
{:err, response.status_code}
|
{:err, response.status_code}
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
defmodule LoLAPI.MatchApi do
|
defmodule LoLAPI.MatchApi do
|
||||||
require Logger
|
require Logger
|
||||||
@match_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/%{matchid}"
|
@match_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/%{matchid}"
|
||||||
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids"
|
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids?queue=420"
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get match by id
|
Get match by id
|
||||||
|
@ -10,8 +10,8 @@ defmodule Scrapper.Application do
|
|||||||
children = [
|
children = [
|
||||||
Scrapper.Queue.MatchQueue,
|
Scrapper.Queue.MatchQueue,
|
||||||
Scrapper.Queue.PlayerQueue,
|
Scrapper.Queue.PlayerQueue,
|
||||||
{Scrapper.Processor.MatchProcessor, []},
|
{Scrapper.Consumer.MatchConsumer, []},
|
||||||
{Scrapper.Processor.PlayerProcessor, []}
|
{Scrapper.Consumer.PlayerConsumer, []}
|
||||||
# Starts a worker by calling: Scrapper.Worker.start_link(arg)
|
# Starts a worker by calling: Scrapper.Worker.start_link(arg)
|
||||||
# {Scrapper.Worker, arg}
|
# {Scrapper.Worker, arg}
|
||||||
]
|
]
|
||||||
|
83
apps/scrapper/lib/scrapper/consumer/match_consumer.ex
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
defmodule Scrapper.Consumer.MatchConsumer do
|
||||||
|
require Logger
|
||||||
|
use Broadway
|
||||||
|
|
||||||
|
def start_link(_opts) do
|
||||||
|
Broadway.start_link(
|
||||||
|
__MODULE__,
|
||||||
|
name: __MODULE__,
|
||||||
|
producer: [
|
||||||
|
module:
|
||||||
|
{BroadwayRabbitMQ.Producer,
|
||||||
|
queue: "match",
|
||||||
|
connection: [
|
||||||
|
username: "guest",
|
||||||
|
password: "guest",
|
||||||
|
host: "localhost"
|
||||||
|
],
|
||||||
|
on_failure: :reject_and_requeue,
|
||||||
|
qos: [
|
||||||
|
prefetch_count: 1
|
||||||
|
]},
|
||||||
|
concurrency: 1,
|
||||||
|
rate_limiting: [
|
||||||
|
interval: 300,
|
||||||
|
allowed_messages: 1
|
||||||
|
]
|
||||||
|
],
|
||||||
|
processors: [
|
||||||
|
default: [
|
||||||
|
concurrency: 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_message(_, message = %Broadway.Message{}, _) do
|
||||||
|
match_id = message.data
|
||||||
|
|
||||||
|
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
|
||||||
|
process_resp(resp, match_id)
|
||||||
|
|
||||||
|
message
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_resp({:ok, raw_match}, match_id) do
|
||||||
|
Task.start_link(fn ->
|
||||||
|
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
|
||||||
|
|
||||||
|
match_url =
|
||||||
|
case decoded_match.info.queueId do
|
||||||
|
420 ->
|
||||||
|
Logger.info("#{match_id} #{decoded_match.info.gameVersion}")
|
||||||
|
|
||||||
|
Storage.MatchStorage.S3MatchStorage.store_match(
|
||||||
|
match_id,
|
||||||
|
raw_match,
|
||||||
|
"ranked",
|
||||||
|
"#{decoded_match.info.gameVersion}"
|
||||||
|
)
|
||||||
|
|
||||||
|
LolAnalytics.Dimensions.Match.MatchRepo.get_or_create(%{
|
||||||
|
match_id: decoded_match.metadata.matchId,
|
||||||
|
patch_number: decoded_match.info.gameVersion,
|
||||||
|
queue_id: 420
|
||||||
|
})
|
||||||
|
|
||||||
|
_queue_id ->
|
||||||
|
Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
|
||||||
|
end
|
||||||
|
|
||||||
|
decoded_match.metadata.participants
|
||||||
|
# |> Enum.shuffle()
|
||||||
|
# |> Enum.take(2)
|
||||||
|
|> Enum.each(fn participant_puuid ->
|
||||||
|
Scrapper.Queue.PlayerQueue.enqueue_puuid(participant_puuid)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_resp({:err, _code}, _match_id) do
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,4 @@
|
|||||||
defmodule Scrapper.Processor.PlayerProcessor do
|
defmodule Scrapper.Consumer.PlayerConsumer do
|
||||||
alias Calendar.ISO
|
|
||||||
use Broadway
|
use Broadway
|
||||||
|
|
||||||
def start_link(_opts) do
|
def start_link(_opts) do
|
||||||
@ -21,7 +20,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
|
|||||||
]},
|
]},
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
rate_limiting: [
|
rate_limiting: [
|
||||||
interval: 1000 * 10,
|
interval: 6700,
|
||||||
allowed_messages: 1
|
allowed_messages: 1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@ -53,7 +52,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
|
|||||||
{
|
{
|
||||||
matches
|
matches
|
||||||
|> Enum.each(fn match_id ->
|
|> Enum.each(fn match_id ->
|
||||||
Scrapper.Queue.MatchQueue.queue_match(match_id)
|
Scrapper.Queue.MatchQueue.enqueue_match(match_id)
|
||||||
end)
|
end)
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,9 @@
|
|||||||
defmodule Scrapper.MatchClassifier do
|
defmodule Scrapper.MatchClassifier do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@spec classify_match(%LoLAPI.Model.MatchResponse{}) :: nil
|
def stream_classify_matches_by_queue(queue \\ 420, bucket \\ "ranked") do
|
||||||
def classify_match(match = %LoLAPI.Model.MatchResponse{}) do
|
Storage.MatchStorage.S3MatchStorage.stream_files("matches")
|
||||||
classify_match_by_queue(match.info.queueId)
|
|> Stream.each(fn match ->
|
||||||
end
|
|
||||||
|
|
||||||
@spec classify_match_by_queue(String.t()) :: nil
|
|
||||||
def classify_match_by_queue("420") do
|
|
||||||
matches = Storage.MatchStorage.S3MatchStorage.list_files("matches")
|
|
||||||
total_matches = Enum.count(matches)
|
|
||||||
|
|
||||||
matches
|
|
||||||
|> Enum.with_index(fn match, index -> {match, index} end)
|
|
||||||
|> Scrapper.Parallel.peach(fn {match, index} ->
|
|
||||||
%{key: json_file} = match
|
%{key: json_file} = match
|
||||||
[key | _] = String.split(json_file, ".")
|
[key | _] = String.split(json_file, ".")
|
||||||
|
|
||||||
@ -25,15 +15,12 @@ defmodule Scrapper.MatchClassifier do
|
|||||||
%{"info" => %{"gameVersion" => gameVersion, "queueId" => queueId}} =
|
%{"info" => %{"gameVersion" => gameVersion, "queueId" => queueId}} =
|
||||||
Poison.decode!(response.body)
|
Poison.decode!(response.body)
|
||||||
|
|
||||||
if queueId == 420 do
|
if queueId == queue do
|
||||||
Storage.MatchStorage.S3MatchStorage.store_match(key, response.body, "ranked", gameVersion)
|
Storage.MatchStorage.S3MatchStorage.store_match(key, response.body, bucket, gameVersion)
|
||||||
Logger.info("Match at #{index} of #{total_matches} is classified")
|
Logger.info("Match #{key} processed")
|
||||||
end
|
end
|
||||||
|
|
||||||
match
|
match
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def classify_match_by_queue(_) do
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
defmodule Scrapper.Processor.MatchProcessor do
|
|
||||||
use Broadway
|
|
||||||
|
|
||||||
def start_link(_opts) do
|
|
||||||
Broadway.start_link(
|
|
||||||
__MODULE__,
|
|
||||||
name: __MODULE__,
|
|
||||||
producer: [
|
|
||||||
module:
|
|
||||||
{BroadwayRabbitMQ.Producer,
|
|
||||||
queue: "match",
|
|
||||||
connection: [
|
|
||||||
username: "guest",
|
|
||||||
password: "guest",
|
|
||||||
host: "localhost"
|
|
||||||
],
|
|
||||||
on_failure: :reject_and_requeue,
|
|
||||||
qos: [
|
|
||||||
prefetch_count: 1
|
|
||||||
]},
|
|
||||||
concurrency: 1,
|
|
||||||
rate_limiting: [
|
|
||||||
interval: 333 * 1,
|
|
||||||
allowed_messages: 1
|
|
||||||
]
|
|
||||||
],
|
|
||||||
processors: [
|
|
||||||
default: [
|
|
||||||
concurrency: 1
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_message(_, message = %Broadway.Message{}, _) do
|
|
||||||
match_id = message.data
|
|
||||||
|
|
||||||
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
|
|
||||||
process_resp(resp, match_id)
|
|
||||||
|
|
||||||
message
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_resp({:ok, raw_match}, match_id) do
|
|
||||||
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
|
|
||||||
match_url = Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
|
|
||||||
match = LolAnalytics.Match.MatchRepo.get_match(match_id)
|
|
||||||
|
|
||||||
case match do
|
|
||||||
nil ->
|
|
||||||
LolAnalytics.Match.MatchRepo.insert_match(match_id)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
LolAnalytics.Match.MatchRepo.update_match(match, %{
|
|
||||||
:processed => true,
|
|
||||||
:match_url => match_url
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
decoded_match.metadata.participants
|
|
||||||
|> Enum.shuffle()
|
|
||||||
|> Enum.take(2)
|
|
||||||
|> Enum.each(fn participant_puuid ->
|
|
||||||
Scrapper.Queue.PlayerQueue.queue_puuid(participant_puuid)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_resp({:err, code}, match_id) do
|
|
||||||
end
|
|
||||||
end
|
|