Skip to content

Commit

Permalink
basic champ stats
Browse files Browse the repository at this point in the history
  • Loading branch information
brianjp93 committed Nov 4, 2024
1 parent 2baf2f8 commit 34e5d02
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 10 deletions.
25 changes: 25 additions & 0 deletions data/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import re
from string import ascii_uppercase

Expand All @@ -7,6 +8,8 @@
from django.contrib.postgres import fields
import logging

from django.utils.functional import cached_property

from core.models import TimestampedModel, VersionedModel, ThumbnailedModel
from data.constants import ITEM_STAT_COSTS

Expand All @@ -19,6 +22,28 @@ class Rito(models.Model):
versions = models.CharField(max_length=10000, default="[]", blank=True)
last_data_import = models.DateTimeField(null=True)

@cached_property
def minor_version_list(self):
versions = json.loads(self.versions)
ret = []
seen = set()
for x in versions:
parts = x.split('.')
if len(parts) != 3:
continue
a, b, c = parts
key = f"{a}.{b}"
if key in seen:
continue
seen.add(key)
ret.append({
"version": f"{a}.{b}",
"major": a,
"minor": b,
"patch": c,
})
return ret

def __str__(self):
return f'Rito(token="{self.token}")'

Expand Down
1 change: 1 addition & 0 deletions lolsite/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
path("", include("player.urls", namespace="player")),
path("", include("match.urls")),
path("", views.Home.as_view(), name="home"),
path("stats", include("stats.urls", namespace="stats")),
path("login/go/", player_views.login_action),
path("logout/", player_views.logout_action, name="logout"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Expand Down
2 changes: 2 additions & 0 deletions player/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from player.serializers import RankPositionSerializer
from player.viewsapi import get_by_puuid
from player import tasks as pt
from stats.views import champion_stats_context


def get_page_urls(request, query_param='page'):
Expand Down Expand Up @@ -113,6 +114,7 @@ def get_context_data(self, *args, **kwargs):
pt.import_positions(self.summoner.id)

context = super().get_context_data(*args, **kwargs)
context.update(champion_stats_context(self.summoner.puuid))
prev_url, next_url = get_page_urls(self.request)
context['next_url'] = next_url
context['prev_url'] = prev_url
Expand Down
20 changes: 20 additions & 0 deletions stats/migrations/0002_summonerchampion_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2024-11-02 19:42

import django.contrib.postgres.fields
import stats.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('stats', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='summonerchampion',
name='version',
field=models.GeneratedField(db_index=True, db_persist=True, expression=stats.models.ArrayConstructor(models.F('major'), models.F('minor')), output_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=2)),
),
]
17 changes: 17 additions & 0 deletions stats/migrations/0003_remove_summonerchampion_dmpm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2024-11-04 02:14

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('stats', '0002_summonerchampion_version'),
]

operations = [
migrations.RemoveField(
model_name='summonerchampion',
name='dmpm',
),
]
20 changes: 20 additions & 0 deletions stats/migrations/0004_summonerchampion_dmpm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2024-11-04 02:14

import django.db.models.expressions
import django.db.models.functions.comparison
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('stats', '0003_remove_summonerchampion_dmpm'),
]

operations = [
migrations.AddField(
model_name='summonerchampion',
name='dmpm',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.db.models.expressions.CombinedExpression(models.F('damage_mitigated'), '/', django.db.models.functions.comparison.Greatest(django.db.models.functions.comparison.Cast('total_seconds', models.FloatField()), 1.0)), '*', models.Value(60.0)), help_text='damage mitigated per minute', output_field=models.FloatField()),
),
]
16 changes: 14 additions & 2 deletions stats/models.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
from functools import cached_property

from django.db import models
from django.db.models import F
from django.db.models import F, Func
from django.db.models.functions import Cast, Greatest
from django.contrib.postgres.fields import ArrayField

from data.models import Champion


class ArrayConstructor(Func):
function = 'ARRAY'
template = '%(function)s[%(expressions)s]'


class SummonerChampion(models.Model):
"""Champion stats for a summoner."""

summoner = models.ForeignKey("player.Summoner", on_delete=models.CASCADE)
champion_key = models.CharField(max_length=32)
major = models.PositiveSmallIntegerField()
minor = models.PositiveSmallIntegerField()
version = models.GeneratedField(
expression=ArrayConstructor(F('major'), F('minor')),
output_field=ArrayField(models.IntegerField(), size=2),
db_persist=True,
db_index=True,
)
queue = models.IntegerField()

game_ids = ArrayField(models.CharField(), default=list)
Expand Down Expand Up @@ -79,7 +91,7 @@ class SummonerChampion(models.Model):
help_text="damage to objectives per minute",
)
dmpm = models.GeneratedField(
expression=F("damage_taken")
expression=F("damage_mitigated")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
output_field=models.FloatField(),
Expand Down
20 changes: 13 additions & 7 deletions stats/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,23 @@ def add_match_to_summoner_champion_stats_all_participants(match):


@app.task
def add_all_matches_for_summoner_to_stats(summoner):
def add_all_matches_for_summoner_to_stats(summoner, major=None, minor=None):
if isinstance(summoner, int):
summoner = Summoner.objects.get(id=summoner)
seen_games = sum(
SummonerChampion.objects.filter(summoner=summoner).values_list(
"game_ids", flat=True
),
start=[],
)
elif isinstance(summoner, str):
summoner = Summoner.objects.get(puuid=summoner)
sc_qs = SummonerChampion.objects.filter(summoner=summoner)
if major:
sc_qs = sc_qs.filter(major=major)
if minor:
sc_qs = sc_qs.filter(minor=minor)
seen_games = sum(sc_qs.values_list("game_ids", flat=True), start=[])
new_matches = Match.objects.filter(participants__puuid=summoner.puuid).exclude(
_id__in=seen_games
)
if major is not None:
new_matches = new_matches.filter(major=major)
if minor is not None:
new_matches = new_matches.filter(minor=minor)
for match in new_matches:
add_match_to_summoner_champion_stats(summoner, match)
10 changes: 10 additions & 0 deletions stats/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path
from . import views

app_name = 'stats'

urlpatterns = [
path("champions/<slug:puuid>/<int:queue>/<int:major>/<int:minor>/", views.champion_stats, name="champions-version"),
path("champions/<slug:puuid>/<int:queue>/<int:major>/", views.champion_stats, name="champions-version"),
path("champions/<slug:puuid>/", views.champion_stats, name="champions-default"),
]
103 changes: 102 additions & 1 deletion stats/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,104 @@
from django.db.models.functions import Cast, Greatest
from django.shortcuts import render
from django.db.models import F, Sum
from django.db import models

# Create your views here.
from stats.models import SummonerChampion
from stats.tasks import add_all_matches_for_summoner_to_stats

from data.models import Rito, Champion


def champion_stats_context(puuid, major=None, minor=None, queue=420):
versions = []
last_major = None
for version in Rito.objects.first().minor_version_list:
if version["major"] != last_major:
last_major = version["major"]
versions.append(
{
"version": f"{version["major"]}.x",
"major": version["major"],
"minor": None,
"patch": None,
}
)
versions.append(version)
if not major:
major = int(versions[1]["major"])
minor = int(versions[1]["minor"])

add_all_matches_for_summoner_to_stats(puuid, major=major, minor=minor)

qs = SummonerChampion.objects.filter(summoner__puuid=puuid)
if major is not None:
qs = qs.filter(major=major)
if minor is not None:
qs = qs.filter(minor=minor)
if queue is not None:
qs = qs.filter(queue=queue)

qs = qs.values("summoner", "champion_key").annotate(
kills=Sum("kills"),
deaths=Sum("deaths"),
assists=Sum("assists"),
damage_to_champions=Sum("damage_to_champions"),
damage_to_objectives=Sum("damage_to_objectives"),
damage_to_turrets=Sum("damage_to_turrets"),
damage_taken=Sum("damage_taken"),
damage_mitigated=Sum("damage_mitigated"),
vision_score=Sum("vision_score"),
total_seconds=Sum("total_seconds"),
wins=Sum("wins"),
losses=Sum("losses"),
vspm=F("vision_score")
/ Greatest(
Cast("total_seconds", models.FloatField()),
1.0,
output_field=models.FloatField(),
)
* 60.0,
kda=(F("kills") + F("assists"))
/ Greatest(Cast("deaths", models.FloatField()), 1.0),
dpm=F("damage_to_champions")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
dtpm=F("damage_taken")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
dttpm=F("damage_to_turrets")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
dtopm=F("damage_to_objectives")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
dmpm=F("damage_mitigated")
/ Greatest(Cast("total_seconds", models.FloatField()), 1.0)
* 60.0,
)

qs = qs.annotate(
game_count=F("wins") + F("losses"),
).order_by("-game_count")

keys = [x["champion_key"] for x in qs]
champions = {
x.key: x
for x in Champion.objects.filter(key__in=keys).order_by("-key").distinct("key")
}
for cs in qs:
cs["champion"] = champions.get(int(cs["champion_key"]), None)

return {
"championstats": qs,
"major": major,
"minor": minor,
"queue": queue,
"puuid": puuid,
"versions": versions[:10],
}


def champion_stats(request, puuid, major=None, minor=None, queue=420):
context = champion_stats_context(puuid, major, minor, queue)
return render(request, "stats/_champions.html", context)
77 changes: 77 additions & 0 deletions templates/cotton/stats/champions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{% load humanize %}
<div class="bg-slate-700 rounded px-2 champion-stats">
<h3>Champion Stats ({{ major }}.{{ minor|default:"x" }})</h3>
<div class="flex gap-x-2">
<div class="flex flex-col gap-y-2">
<button
{% if minor is not None %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=major minor=minor queue=420 %}"
{% else %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=major queue=420 %}"
{% endif %}
hx-target="closest .champion-stats"
hx-swap="outerHTML"
class="btn btn-default"
>
Solo Queue
</button>
<button
{% if minor is not None %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=major minor=minor queue=440 %}"
{% else %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=major queue=440 %}"
{% endif %}
hx-target="closest .champion-stats"
hx-swap="outerHTML"
class="btn btn-default"
>
Flex Queue
</button>
</div>
<div class="flex flex-col gap-y-2 gap-x-1 h-[100px] flex-wrap">
{% for version in versions %}
<button
{% if version.minor %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=version.major minor=version.minor queue=queue %}"
{% else %}
hx-get="{% url 'stats:champions-version' puuid=puuid major=version.major queue=queue %}"
{% endif %}
hx-swap="outerHTML"
hx-target="closest .champion-stats"
class="btn btn-default">
{{ version.version }}
</button>
{% endfor %}
</div>
</div>
<div class="flex gap-2 max-h-[400px] overflow-y-scroll flex-wrap">
{% for stat in championstats %}
<div class="flex flex-col p-2 border border-white rounded">
<div class="flex gap-x-2">
<img src="{{ stat.champion.image_url }}" class="w-8 h-8" />
<div>
{{ stat.champion.name }} ({{ stat.wins }} / {{ stat.losses }})
</div>
</div>
<div>
<b>{{ stat.kda|floatformat:2 }}</b> KDA
</div>
<div>
<b>{{ stat.dpm|intcomma|floatformat:0 }}</b> DPM
</div>
<div>
<b>{{ stat.vspm|intcomma|floatformat:1 }}</b> VSPM
</div>
<div>
<b>{{ stat.dtpm|intcomma|floatformat:0 }}</b> Damage Taken per Minute
</div>
<div>
<b>{{ stat.dttpm|intcomma|floatformat:0 }}</b> Tower Damage per Minute
</div>
<div>
<b>{{ stat.dtopm|intcomma|floatformat:0 }}</b> Objective Damage per Minute
</div>
</div>
{% endfor %}
</div>
</div>
Loading

0 comments on commit 34e5d02

Please sign in to comment.