noita leaderboard + view + management commands
This commit is contained in:
parent
52a6a4adb2
commit
fa53d74295
@ -5,6 +5,7 @@ from django.db.models import (
|
|||||||
Case,
|
Case,
|
||||||
When,
|
When,
|
||||||
Sum,
|
Sum,
|
||||||
|
Count,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
Subquery,
|
Subquery,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
@ -154,7 +155,6 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
|
|
||||||
# Get unique users and their scores, then apply ranking
|
# Get unique users and their scores, then apply ranking
|
||||||
leaderboard = (
|
leaderboard = (
|
||||||
# User.objects.filter(objectiv_set__isnull=False)
|
|
||||||
User.objects.filter(objectiv__isnull=False)
|
User.objects.filter(objectiv__isnull=False)
|
||||||
.distinct()
|
.distinct()
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -163,13 +163,14 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(objectives_count=Count("objectiv", distinct=True))
|
||||||
.annotate(
|
.annotate(
|
||||||
rank=Window(
|
rank=Window(
|
||||||
expression=Rank(),
|
expression=Rank(),
|
||||||
order_by=F("total_score").desc(),
|
order_by=F("total_score").desc(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.values("rank", "username", "total_score")
|
.values("rank", "username", "total_score", "objectives_count")
|
||||||
.order_by("rank")
|
.order_by("rank")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -179,13 +180,14 @@ def get_leaderboard(request: HttpRequest):
|
|||||||
"rank": entry["rank"],
|
"rank": entry["rank"],
|
||||||
"username": entry["username"],
|
"username": entry["username"],
|
||||||
"total_score": entry["total_score"] or 0,
|
"total_score": entry["total_score"] or 0,
|
||||||
|
"objectives_count": entry["objectives_count"],
|
||||||
}
|
}
|
||||||
for entry in leaderboard
|
for entry in leaderboard
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("submit", response=NoitaSubmissionOut)
|
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
||||||
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||||
"""
|
"""
|
||||||
Submit a Noita run file (log file, screenshot, or video).
|
Submit a Noita run file (log file, screenshot, or video).
|
||||||
@ -200,6 +202,7 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
|||||||
# Validate file type
|
# Validate file type
|
||||||
allowed_types = [
|
allowed_types = [
|
||||||
"text/plain",
|
"text/plain",
|
||||||
|
"text/x-log",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/jpg",
|
"image/jpg",
|
||||||
"image/png",
|
"image/png",
|
||||||
|
|||||||
0
polylan_submitter/noita/management/__init__.py
Normal file
0
polylan_submitter/noita/management/__init__.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from noita.models import ObjectivPoint
|
||||||
|
from noita.services.decode import POINTS
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Load ObjectivPoints from the POINTS dictionary in services.decode"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--clear",
|
||||||
|
action="store_true",
|
||||||
|
help="Clear all existing ObjectivPoints before loading",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options["clear"]:
|
||||||
|
ObjectivPoint.objects.all().delete()
|
||||||
|
self.stdout.write(self.style.SUCCESS("Cleared existing ObjectivPoints"))
|
||||||
|
|
||||||
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
for objectiv_id, point_value in POINTS.items():
|
||||||
|
# Skip special entries
|
||||||
|
if objectiv_id in {"-", "DEBUG"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get display string from objectiv_id (convert to title case)
|
||||||
|
display_string = objectiv_id.replace("_", " ").title()
|
||||||
|
|
||||||
|
# Create or update ObjectivPoint
|
||||||
|
obj, created = ObjectivPoint.objects.get_or_create(
|
||||||
|
objectiv_id=objectiv_id,
|
||||||
|
defaults={
|
||||||
|
"display_string": display_string,
|
||||||
|
"point": point_value,
|
||||||
|
"max_count": 1, # Default max_count is 1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
self.stdout.write(f"✓ Created: {objectiv_id} - {point_value} points")
|
||||||
|
else:
|
||||||
|
# Update if points changed
|
||||||
|
if obj.point != point_value or obj.display_string != display_string:
|
||||||
|
obj.point = point_value
|
||||||
|
obj.display_string = display_string
|
||||||
|
obj.save()
|
||||||
|
updated_count += 1
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f"↻ Updated: {objectiv_id} - {point_value} points"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"\nCreated: {created_count}"))
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Updated: {updated_count}"))
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"Total ObjectivPoints: {ObjectivPoint.objects.count()}")
|
||||||
|
)
|
||||||
@ -37,6 +37,7 @@ class LeaderboardEntryOut(Schema):
|
|||||||
rank: int
|
rank: int
|
||||||
username: str
|
username: str
|
||||||
total_score: int
|
total_score: int
|
||||||
|
objectives_count: int
|
||||||
|
|
||||||
|
|
||||||
class LeaderboardOut(Schema):
|
class LeaderboardOut(Schema):
|
||||||
|
|||||||
@ -19,6 +19,8 @@ const isDragover = ref(false);
|
|||||||
const objectives = ref<Objective[]>([]);
|
const objectives = ref<Objective[]>([]);
|
||||||
const isLoadingObjectives = ref(false);
|
const isLoadingObjectives = ref(false);
|
||||||
const isLoadingLeaderboard = ref(false);
|
const isLoadingLeaderboard = ref(false);
|
||||||
|
const leaderboard = ref<any[]>([]);
|
||||||
|
const isLeaderboardModalOpen = ref(false);
|
||||||
|
|
||||||
const handleFileUpload = (event: Event) => {
|
const handleFileUpload = (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@ -127,10 +129,11 @@ const fetchLeaderboard = async () => {
|
|||||||
const response = await fetch("/api/noita/leaderboard");
|
const response = await fetch("/api/noita/leaderboard");
|
||||||
if (!response.ok) throw new Error("Failed to fetch leaderboard");
|
if (!response.ok) throw new Error("Failed to fetch leaderboard");
|
||||||
|
|
||||||
const leaderboard = await response.json();
|
const data = await response.json();
|
||||||
|
leaderboard.value = data.leaderboard;
|
||||||
|
|
||||||
// Find current user's rank
|
// Find current user's rank
|
||||||
const userRank = leaderboard.leaderboard.find(
|
const userRank = leaderboard.value.find(
|
||||||
(entry: any) => entry.username === userInfo.value.username
|
(entry: any) => entry.username === userInfo.value.username
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -230,8 +233,8 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-outline btn-sm w-full mt-6">
|
<button @click="isLeaderboardModalOpen = true" class="btn btn-outline btn-sm w-full mt-6">
|
||||||
<i class="mdi mdi-refresh mr-1"></i>
|
<i class="mdi mdi-trophy mr-1"></i>
|
||||||
View Full Leaderboard
|
View Full Leaderboard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -341,5 +344,76 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Leaderboard Modal -->
|
||||||
|
<div v-if="isLeaderboardModalOpen" class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-4xl">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="font-bold text-lg">
|
||||||
|
<i class="mdi mdi-trophy text-yellow-500 mr-2"></i>
|
||||||
|
Global Leaderboard
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="isLeaderboardModalOpen = false"
|
||||||
|
class="btn btn-sm btn-circle btn-ghost"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leaderboard Table -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th class="text-right">Objectives</th>
|
||||||
|
<th class="text-right">Score</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="entry in leaderboard"
|
||||||
|
:key="entry.username"
|
||||||
|
:class="{ 'bg-primary/20': entry.username === userInfo.username }"
|
||||||
|
>
|
||||||
|
<td class="font-bold">
|
||||||
|
<span
|
||||||
|
v-if="entry.rank === 1"
|
||||||
|
class="badge badge-warning badge-lg"
|
||||||
|
>
|
||||||
|
🏆 #{{ entry.rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="entry.rank === 2" class="badge badge-lg">
|
||||||
|
🥈 #{{ entry.rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="entry.rank === 3" class="badge badge-lg">
|
||||||
|
🥉 #{{ entry.rank }}
|
||||||
|
</span>
|
||||||
|
<span v-else>#{{ entry.rank }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="font-medium">
|
||||||
|
{{ entry.username }}
|
||||||
|
<span
|
||||||
|
v-if="entry.username === userInfo.username"
|
||||||
|
class="badge badge-primary badge-sm ml-2"
|
||||||
|
>
|
||||||
|
You
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ entry.objectives_count }}</td>
|
||||||
|
<td class="text-right font-bold">{{ entry.total_score.toLocaleString() }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="leaderboard.length === 0" class="text-center py-8">
|
||||||
|
<p class="text-base-content/70">No entries yet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop" @click="isLeaderboardModalOpen = false"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user