Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1844b973e9
|
||
|
|
6cee1135a8
|
@@ -7,8 +7,8 @@ from .models import PixelPlacement, PlaceSettings
|
|||||||
|
|
||||||
@admin.register(PlaceSettings)
|
@admin.register(PlaceSettings)
|
||||||
class PlaceSettingsAdmin(DjangoObjectActions, admin.ModelAdmin):
|
class PlaceSettingsAdmin(DjangoObjectActions, admin.ModelAdmin):
|
||||||
list_display = ("event", "width", "height")
|
list_display = ("event", "width", "height", "placement_enabled")
|
||||||
list_editable = ("width", "height")
|
list_editable = ("width", "height", "placement_enabled")
|
||||||
list_filter = ("event",)
|
list_filter = ("event",)
|
||||||
change_actions = ("resync_canvas",)
|
change_actions = ("resync_canvas",)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from .heatmap import (
|
|||||||
build_heatmap_png,
|
build_heatmap_png,
|
||||||
)
|
)
|
||||||
from .models import PixelPlacement, PlaceSettings
|
from .models import PixelPlacement, PlaceSettings
|
||||||
|
from .schemas import PlaceStatsOut
|
||||||
|
from .stats import build_place_stats
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -104,6 +106,14 @@ def heatmap_decay_gif(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(f"{PREFIX}/stats", auth=django_auth_is_staff, response=PlaceStatsOut)
|
||||||
|
@decorate_view(require_game_enabled(path="place"))
|
||||||
|
def stats(request: HttpRequest, event_path: str):
|
||||||
|
"""Admin-only: aggregated stats about the game - totals, coverage, top
|
||||||
|
players, most contested pixels, color usage and activity per hour."""
|
||||||
|
return build_place_stats(request.event)
|
||||||
|
|
||||||
|
|
||||||
@router.get(f"{PREFIX}/placements.csv", auth=django_auth_is_staff)
|
@router.get(f"{PREFIX}/placements.csv", auth=django_auth_is_staff)
|
||||||
@decorate_view(require_game_enabled(path="place"))
|
@decorate_view(require_game_enabled(path="place"))
|
||||||
def placements_csv(request: HttpRequest, event_path: str):
|
def placements_csv(request: HttpRequest, event_path: str):
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ class PlaceConsumer(AsyncWebsocketConsumer):
|
|||||||
await self.send(text_data=encode("r", "a"))
|
await self.send(text_data=encode("r", "a"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not place_settings.placement_enabled:
|
||||||
|
await self.send(text_data=encode("r", "d"))
|
||||||
|
return
|
||||||
|
|
||||||
parts = text_data.split("|")
|
parts = text_data.split("|")
|
||||||
if len(parts) != 4:
|
if len(parts) != 4:
|
||||||
await self.send(text_data=encode("r", "i"))
|
await self.send(text_data=encode("r", "i"))
|
||||||
@@ -142,6 +146,7 @@ class PlaceConsumer(AsyncWebsocketConsumer):
|
|||||||
next_placement_at,
|
next_placement_at,
|
||||||
place_settings.width,
|
place_settings.width,
|
||||||
place_settings.height,
|
place_settings.height,
|
||||||
|
int(place_settings.placement_enabled),
|
||||||
)
|
)
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
@@ -152,4 +157,11 @@ class PlaceConsumer(AsyncWebsocketConsumer):
|
|||||||
await self.send(text_data=encode("p", event["x"], event["y"], event["c"]))
|
await self.send(text_data=encode("p", event["x"], event["y"], event["c"]))
|
||||||
|
|
||||||
async def settings_changed(self, event):
|
async def settings_changed(self, event):
|
||||||
await self.send(text_data=encode("e", event["width"], event["height"]))
|
await self.send(
|
||||||
|
text_data=encode(
|
||||||
|
"e",
|
||||||
|
event["width"],
|
||||||
|
event["height"],
|
||||||
|
int(event["placement_enabled"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-10-25 09:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("place", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="placesettings",
|
||||||
|
name="placement_enabled",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -47,6 +47,9 @@ class PlaceSettings(models.Model):
|
|||||||
)
|
)
|
||||||
width = models.PositiveSmallIntegerField(default=DEFAULT_GRID_WIDTH)
|
width = models.PositiveSmallIntegerField(default=DEFAULT_GRID_WIDTH)
|
||||||
height = models.PositiveSmallIntegerField(default=DEFAULT_GRID_HEIGHT)
|
height = models.PositiveSmallIntegerField(default=DEFAULT_GRID_HEIGHT)
|
||||||
|
# Freezes the board without taking the game down: clients can still
|
||||||
|
# connect and watch, but every placement is rejected.
|
||||||
|
placement_enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"PlaceSettings({self.event}, size={self.width}x{self.height})"
|
return f"PlaceSettings({self.event}, size={self.width}x{self.height})"
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ninja import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerCountOut(Schema):
|
||||||
|
username: str
|
||||||
|
placements: int
|
||||||
|
|
||||||
|
|
||||||
|
class PixelCountOut(Schema):
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
placements: int
|
||||||
|
players: int
|
||||||
|
|
||||||
|
|
||||||
|
class ColorCountOut(Schema):
|
||||||
|
color_index: int
|
||||||
|
color: str
|
||||||
|
placements: int
|
||||||
|
# How many pixels of the live board currently have this color
|
||||||
|
on_canvas: int
|
||||||
|
|
||||||
|
|
||||||
|
class HourCountOut(Schema):
|
||||||
|
hour: datetime
|
||||||
|
placements: int
|
||||||
|
players: int
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceStatsOut(Schema):
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
total_placements: int
|
||||||
|
total_players: int
|
||||||
|
first_placement_at: datetime | None
|
||||||
|
last_placement_at: datetime | None
|
||||||
|
# Distinct pixels placed at least once (within the current grid)
|
||||||
|
pixels_touched: int
|
||||||
|
coverage: float
|
||||||
|
# Placements that landed on an already placed pixel
|
||||||
|
overwrites: int
|
||||||
|
avg_placements_per_player: float
|
||||||
|
top_players: list[PlayerCountOut]
|
||||||
|
most_contested_pixels: list[PixelCountOut]
|
||||||
|
colors: list[ColorCountOut]
|
||||||
|
per_hour: list[HourCountOut]
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Reacts to an admin editing a PlaceSettings row (grid size) by:
|
"""Reacts to an admin editing a PlaceSettings row (grid size, placement
|
||||||
|
toggle) by:
|
||||||
|
|
||||||
1. Rebuilding that event's Redis canvas from history at the (possibly new)
|
1. Rebuilding that event's Redis canvas from history at the (possibly new)
|
||||||
dimensions - a resize invalidates the old Redis bytes (wrong length,
|
dimensions - a resize invalidates the old Redis bytes (wrong length,
|
||||||
wrong meaning), so this keeps Redis self-healing instead of relying on
|
wrong meaning), so this keeps Redis self-healing instead of relying on
|
||||||
the admin remembering to click "Resync canvas from DB" after every
|
the admin remembering to click "Resync canvas from DB" after every
|
||||||
resize.
|
resize.
|
||||||
2. Pushing the new size to that event's already-connected clients. Unlike
|
2. Pushing the new settings to that event's already-connected clients. Unlike
|
||||||
the clock app, place has no periodic heartbeat broadcast to piggyback
|
the clock app, place has no periodic heartbeat broadcast to piggyback
|
||||||
this onto, so it's broadcast explicitly here instead.
|
this onto, so it's broadcast explicitly here instead.
|
||||||
"""
|
"""
|
||||||
@@ -33,5 +34,6 @@ def on_settings_saved(sender, instance, **kwargs):
|
|||||||
"type": "settings_changed",
|
"type": "settings_changed",
|
||||||
"width": instance.width,
|
"width": instance.width,
|
||||||
"height": instance.height,
|
"height": instance.height,
|
||||||
|
"placement_enabled": instance.placement_enabled,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""Aggregated stats about one event's place game, for the admin stats
|
||||||
|
endpoint. Everything comes from the durable PixelPlacement history, except
|
||||||
|
the colors currently on the board, which are read off the live Redis canvas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db.models import Count, Max, Min
|
||||||
|
from django.db.models.functions import TruncHour
|
||||||
|
|
||||||
|
from events.models import Event
|
||||||
|
|
||||||
|
from .canvas import read_canvas
|
||||||
|
from .models import PALETTE, PixelPlacement, PlaceSettings
|
||||||
|
|
||||||
|
TOP_N = 10
|
||||||
|
|
||||||
|
|
||||||
|
def build_place_stats(event: Event) -> dict:
|
||||||
|
place_settings = PlaceSettings.current(event)
|
||||||
|
width, height = place_settings.width, place_settings.height
|
||||||
|
|
||||||
|
placements = PixelPlacement.objects.filter(event=event)
|
||||||
|
totals = placements.aggregate(
|
||||||
|
total=Count("id"),
|
||||||
|
players=Count("player", distinct=True),
|
||||||
|
first=Min("placed_at"),
|
||||||
|
last=Max("placed_at"),
|
||||||
|
)
|
||||||
|
total, players = totals["total"], totals["players"]
|
||||||
|
|
||||||
|
# Same as heatmap.py, placements outside the current grid are skipped
|
||||||
|
in_grid = placements.filter(x__lt=width, y__lt=height)
|
||||||
|
pixels_touched = in_grid.values("x", "y").distinct().count()
|
||||||
|
|
||||||
|
top_players = (
|
||||||
|
placements.values("player__username")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
.order_by("-count", "player__username")[:TOP_N]
|
||||||
|
)
|
||||||
|
contested = (
|
||||||
|
in_grid.values("x", "y")
|
||||||
|
.annotate(count=Count("id"), players=Count("player", distinct=True))
|
||||||
|
.order_by("-count", "-players", "y", "x")[:TOP_N]
|
||||||
|
)
|
||||||
|
color_counts = dict(
|
||||||
|
placements.values_list("color_index")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
.order_by()
|
||||||
|
)
|
||||||
|
# Redis only pads up to the last written pixel, and keeps old bytes
|
||||||
|
# after a shrink, so normalize to the current grid size
|
||||||
|
size = width * height
|
||||||
|
canvas = read_canvas(event.path, width, height)[:size].ljust(size, b"\0")
|
||||||
|
per_hour = (
|
||||||
|
placements.annotate(hour=TruncHour("placed_at"))
|
||||||
|
.values("hour")
|
||||||
|
.annotate(count=Count("id"), players=Count("player", distinct=True))
|
||||||
|
.order_by("hour")
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"total_placements": total,
|
||||||
|
"total_players": players,
|
||||||
|
"first_placement_at": totals["first"],
|
||||||
|
"last_placement_at": totals["last"],
|
||||||
|
"pixels_touched": pixels_touched,
|
||||||
|
"coverage": pixels_touched / size if size else 0.0,
|
||||||
|
"overwrites": in_grid.count() - pixels_touched,
|
||||||
|
"avg_placements_per_player": total / players if players else 0.0,
|
||||||
|
"top_players": [
|
||||||
|
{"username": row["player__username"], "placements": row["count"]}
|
||||||
|
for row in top_players
|
||||||
|
],
|
||||||
|
"most_contested_pixels": [
|
||||||
|
{
|
||||||
|
"x": row["x"],
|
||||||
|
"y": row["y"],
|
||||||
|
"placements": row["count"],
|
||||||
|
"players": row["players"],
|
||||||
|
}
|
||||||
|
for row in contested
|
||||||
|
],
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"color_index": index,
|
||||||
|
"color": color,
|
||||||
|
"placements": color_counts.get(index, 0),
|
||||||
|
"on_canvas": canvas.count(index),
|
||||||
|
}
|
||||||
|
for index, color in enumerate(PALETTE)
|
||||||
|
],
|
||||||
|
"per_hour": [
|
||||||
|
{"hour": row["hour"], "placements": row["count"], "players": row["players"]}
|
||||||
|
for row in per_hour
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from accounts.models import CustomUser
|
||||||
|
from events.models import Event
|
||||||
|
from games.models import EventGame, Game
|
||||||
|
|
||||||
|
from .models import PixelPlacement, PlaceSettings
|
||||||
|
|
||||||
|
URL = "/api/events/event/place/stats"
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("place.stats.read_canvas")
|
||||||
|
class PlaceStatsTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.event = Event.objects.create(name="Event", path="event")
|
||||||
|
place = Game.objects.create(name="Place", path="place", custom_path=True)
|
||||||
|
EventGame.objects.create(event=self.event, game=place)
|
||||||
|
PlaceSettings.objects.create(event=self.event, width=4, height=2)
|
||||||
|
self.alice = CustomUser.objects.create_user(username="alice", password="x")
|
||||||
|
self.bob = CustomUser.objects.create_user(username="bob", password="x")
|
||||||
|
self.admin = CustomUser.objects.create_user(
|
||||||
|
username="admin", password="x", is_staff=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def place(self, player, x, y, color_index):
|
||||||
|
PixelPlacement.objects.create(
|
||||||
|
event=self.event, player=player, x=x, y=y, color_index=color_index
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_requires_staff(self, _):
|
||||||
|
self.client.force_login(self.alice)
|
||||||
|
|
||||||
|
response = self.client.get(URL)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
def test_empty_game(self, read_canvas):
|
||||||
|
read_canvas.return_value = b""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
|
||||||
|
data = self.client.get(URL).json()
|
||||||
|
|
||||||
|
self.assertEqual(data["total_placements"], 0)
|
||||||
|
self.assertEqual(data["total_players"], 0)
|
||||||
|
self.assertEqual(data["coverage"], 0.0)
|
||||||
|
self.assertEqual(data["avg_placements_per_player"], 0.0)
|
||||||
|
self.assertEqual(data["colors"][0]["on_canvas"], 8)
|
||||||
|
self.assertEqual(data["per_hour"], [])
|
||||||
|
|
||||||
|
def test_stats(self, read_canvas):
|
||||||
|
self.place(self.alice, 0, 0, 5)
|
||||||
|
self.place(self.bob, 0, 0, 3)
|
||||||
|
self.place(self.alice, 0, 0, 5)
|
||||||
|
self.place(self.alice, 1, 0, 3)
|
||||||
|
# Outside the current grid, e.g. from before a shrink
|
||||||
|
self.place(self.bob, 9, 9, 3)
|
||||||
|
# Short canvas, Redis only padded up to the last written pixel
|
||||||
|
read_canvas.return_value = bytes([5, 3])
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
|
||||||
|
data = self.client.get(URL).json()
|
||||||
|
|
||||||
|
self.assertEqual(data["total_placements"], 5)
|
||||||
|
self.assertEqual(data["total_players"], 2)
|
||||||
|
self.assertEqual(data["pixels_touched"], 2)
|
||||||
|
self.assertEqual(data["coverage"], 0.25)
|
||||||
|
self.assertEqual(data["overwrites"], 2)
|
||||||
|
self.assertEqual(data["avg_placements_per_player"], 2.5)
|
||||||
|
self.assertEqual(
|
||||||
|
data["top_players"],
|
||||||
|
[
|
||||||
|
{"username": "alice", "placements": 3},
|
||||||
|
{"username": "bob", "placements": 2},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
data["most_contested_pixels"][0],
|
||||||
|
{"x": 0, "y": 0, "placements": 3, "players": 2},
|
||||||
|
)
|
||||||
|
colors = {row["color_index"]: row for row in data["colors"]}
|
||||||
|
self.assertEqual(colors[5]["placements"], 2)
|
||||||
|
self.assertEqual(colors[3]["placements"], 3)
|
||||||
|
self.assertEqual(colors[5]["on_canvas"], 1)
|
||||||
|
self.assertEqual(colors[0]["on_canvas"], 6)
|
||||||
|
self.assertEqual(sum(row["placements"] for row in data["per_hour"]), 5)
|
||||||
@@ -6,8 +6,10 @@ either). Each message is a short string: a one-letter type tag followed by
|
|||||||
'|'-separated fields, in a fixed order the client already knows from this
|
'|'-separated fields, in a fixed order the client already knows from this
|
||||||
table:
|
table:
|
||||||
|
|
||||||
s|<next>|<width>|<height> initial personal state, once per connection
|
s|<next>|<width>|<height>|<on>
|
||||||
(next = epoch ms cooldown ends, or empty if free now)
|
initial personal state, once per connection
|
||||||
|
(next = epoch ms cooldown ends, or empty if free now;
|
||||||
|
on = 1 if placing pixels is allowed, 0 if frozen)
|
||||||
p|<x>|<y>|<c> a pixel was placed - broadcast to everyone,
|
p|<x>|<y>|<c> a pixel was placed - broadcast to everyone,
|
||||||
including the placer (no separate ack needed)
|
including the placer (no separate ack needed)
|
||||||
k|<next> your own placement was accepted; carries your
|
k|<next> your own placement was accepted; carries your
|
||||||
@@ -15,10 +17,13 @@ table:
|
|||||||
r|c|<next> rejected: still on cooldown
|
r|c|<next> rejected: still on cooldown
|
||||||
r|a rejected: not logged in
|
r|a rejected: not logged in
|
||||||
r|i rejected: invalid x/y/color
|
r|i rejected: invalid x/y/color
|
||||||
e|<width>|<height> an admin resized the grid for this event -
|
r|d rejected: placement is disabled by an admin
|
||||||
broadcast to everyone. A client that sees this
|
e|<width>|<height>|<on>
|
||||||
should re-fetch /api/.../place/canvas rather than
|
an admin edited this event's settings (grid size
|
||||||
try to remap its local pixels.
|
and/or placement toggle) - broadcast to everyone.
|
||||||
|
If the size changed, a client should re-fetch
|
||||||
|
/api/.../place/canvas rather than try to remap its
|
||||||
|
local pixels.
|
||||||
|
|
||||||
Client -> server has exactly one message shape, the same as the broadcast:
|
Client -> server has exactly one message shape, the same as the broadcast:
|
||||||
p|<x>|<y>|<c>
|
p|<x>|<y>|<c>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function drawPixel(x: number, y: number, colorIndex: number) {
|
|||||||
const {
|
const {
|
||||||
connected,
|
connected,
|
||||||
cooldownRemainingMs,
|
cooldownRemainingMs,
|
||||||
|
placementEnabled,
|
||||||
cooldownDurationSeconds,
|
cooldownDurationSeconds,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight,
|
gridHeight,
|
||||||
@@ -55,6 +56,7 @@ const overlayHeight = computed(() => gridHeight.value * PX_PER_CELL);
|
|||||||
|
|
||||||
const cooldownSecondsDisplay = computed(() => (cooldownRemainingMs.value / 1000).toFixed(1));
|
const cooldownSecondsDisplay = computed(() => (cooldownRemainingMs.value / 1000).toFixed(1));
|
||||||
const onCooldown = computed(() => cooldownRemainingMs.value > 0);
|
const onCooldown = computed(() => cooldownRemainingMs.value > 0);
|
||||||
|
const canPlace = computed(() => placementEnabled.value && !onCooldown.value);
|
||||||
|
|
||||||
// The overlay canvas never touches the pixel colors - it only draws grid
|
// The overlay canvas never touches the pixel colors - it only draws grid
|
||||||
// lines plus a preview of the hovered cell, so it can freely redraw on
|
// lines plus a preview of the hovered cell, so it can freely redraw on
|
||||||
@@ -86,7 +88,7 @@ function drawOverlay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoveredCell.value && !onCooldown.value) {
|
if (hoveredCell.value && canPlace.value) {
|
||||||
const { x, y } = hoveredCell.value;
|
const { x, y } = hoveredCell.value;
|
||||||
gridCtx.fillStyle = `${PALETTE[selectedColorIndex.value]}80`;
|
gridCtx.fillStyle = `${PALETTE[selectedColorIndex.value]}80`;
|
||||||
gridCtx.fillRect(x * PX_PER_CELL, y * PX_PER_CELL, PX_PER_CELL, PX_PER_CELL);
|
gridCtx.fillRect(x * PX_PER_CELL, y * PX_PER_CELL, PX_PER_CELL, PX_PER_CELL);
|
||||||
@@ -96,7 +98,7 @@ function drawOverlay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([selectedColorIndex, onCooldown, showGrid, gridWidth, gridHeight], drawOverlay);
|
watch([selectedColorIndex, canPlace, showGrid, gridWidth, gridHeight], drawOverlay);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
pixelCtx = pixelCanvasRef.value?.getContext("2d") ?? null;
|
pixelCtx = pixelCanvasRef.value?.getContext("2d") ?? null;
|
||||||
@@ -125,7 +127,7 @@ function handleMouseLeave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(event: MouseEvent) {
|
function handleClick(event: MouseEvent) {
|
||||||
if (onCooldown.value) return;
|
if (!canPlace.value) return;
|
||||||
const { x, y } = cellFromEvent(event);
|
const { x, y } = cellFromEvent(event);
|
||||||
place(x, y, selectedColorIndex.value);
|
place(x, y, selectedColorIndex.value);
|
||||||
}
|
}
|
||||||
@@ -142,7 +144,7 @@ function handleClick(event: MouseEvent) {
|
|||||||
<canvas ref="pixelCanvasRef" :width="gridWidth" :height="gridHeight"
|
<canvas ref="pixelCanvasRef" :width="gridWidth" :height="gridHeight"
|
||||||
class="absolute inset-0 w-full h-full border border-base-300 rounded-box shadow-lg" :style="{
|
class="absolute inset-0 w-full h-full border border-base-300 rounded-box shadow-lg" :style="{
|
||||||
imageRendering: 'pixelated',
|
imageRendering: 'pixelated',
|
||||||
cursor: onCooldown ? 'not-allowed' : 'crosshair',
|
cursor: canPlace ? 'crosshair' : 'not-allowed',
|
||||||
}" @click="handleClick" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave" />
|
}" @click="handleClick" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave" />
|
||||||
<canvas ref="gridCanvasRef" :width="overlayWidth" :height="overlayHeight"
|
<canvas ref="gridCanvasRef" :width="overlayWidth" :height="overlayHeight"
|
||||||
class="absolute inset-0 w-full h-full rounded-box pointer-events-none" />
|
class="absolute inset-0 w-full h-full rounded-box pointer-events-none" />
|
||||||
@@ -154,7 +156,8 @@ function handleClick(event: MouseEvent) {
|
|||||||
<button class="btn btn-ghost btn-sm" @click="showGrid = !showGrid">
|
<button class="btn btn-ghost btn-sm" @click="showGrid = !showGrid">
|
||||||
{{ showGrid ? t("place.hideGrid") : t("place.showGrid") }}
|
{{ showGrid ? t("place.hideGrid") : t("place.showGrid") }}
|
||||||
</button>
|
</button>
|
||||||
<template v-if="onCooldown">
|
<span v-if="!placementEnabled" class="badge badge-warning">{{ t("place.placementDisabled") }}</span>
|
||||||
|
<template v-else-if="onCooldown">
|
||||||
<span class="font-bold text-xl tabular-nums">{{ cooldownSecondsDisplay }}s</span>
|
<span class="font-bold text-xl tabular-nums">{{ cooldownSecondsDisplay }}s</span>
|
||||||
<progress class="progress progress-primary w-48"
|
<progress class="progress progress-primary w-48"
|
||||||
:value="cooldownDurationSeconds - cooldownRemainingMs / 1000" :max="cooldownDurationSeconds" />
|
:value="cooldownDurationSeconds - cooldownRemainingMs / 1000" :max="cooldownDurationSeconds" />
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/vue-query';
|
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/vue-query';
|
||||||
|
|
||||||
import { client } from '../client.gen';
|
import { client } from '../client.gen';
|
||||||
import { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, clockApiCurrentState, clockApiMyStats, gamesApiListEventGames, gamesApiListEventPlanning, gamesApiListEventSteamGames, gamesApiListGames, marketApiBetHistory, marketApiCloseMarket, marketApiCreateBet, marketApiGetUserPoints, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponseFiles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, placeApiCanvas, placeApiEvolutionGif, placeApiHeatmapDecayGif, placeApiHeatmapEvolutionGif, placeApiHeatmapPng, placeApiPlacementsCsv, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr, submissionsApiStopSyncScopes, submissionsApiSyncScopes, submissionsApiSyncScopesStatus } from '../sdk.gen';
|
import { animationsApiPuzzleResults, animationsApiResults, animationsApiTopSubmissions, clockApiCurrentState, clockApiMyStats, gamesApiListEventGames, gamesApiListEventPlanning, gamesApiListEventSteamGames, gamesApiListGames, marketApiBetHistory, marketApiCloseMarket, marketApiCreateBet, marketApiGetUserPoints, marketApiListMarkets, marketApiListUserBets, marketApiResolveMarket, noitaApiGetLeaderboard, noitaApiGetResults, noitaApiSubmitLogFile, type Options, opusMagnumApiCreateSubmission, opusMagnumApiDeleteSubmission, opusMagnumApiGetCollection, opusMagnumApiGetStats, opusMagnumApiGetSubmission, opusMagnumApiListPuzzles, opusMagnumApiListResponseFiles, opusMagnumApiListResponsesNeedingValidation, opusMagnumApiListSubmissions, opusMagnumApiValidateAuto, opusMagnumApiValidateResponse, opusMagnumApiValidateSubmission, placeApiCanvas, placeApiEvolutionGif, placeApiHeatmapDecayGif, placeApiHeatmapEvolutionGif, placeApiHeatmapPng, placeApiPlacementsCsv, placeApiStats, polylanSubmitterApiClearCache, polylanSubmitterApiGetUserInfo, polylanSubmitterApiHealthCheck, submissionsApiCreateImageCrop, submissionsApiCreateSubmission, submissionsApiDeleteImageCrop, submissionsApiGetSubmission, submissionsApiListAvailableScopes, submissionsApiListMyScopes, submissionsApiListSubmissionsByGame, submissionsApiListUserSubmissions, submissionsApiProcessCropOcr, submissionsApiProcessFileOcr, submissionsApiStopSyncScopes, submissionsApiSyncScopes, submissionsApiSyncScopesStatus } from '../sdk.gen';
|
||||||
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiMyStatsData, ClockApiMyStatsResponse, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventPlanningData, GamesApiListEventPlanningResponse, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponse, GamesApiListGamesData, GamesApiListGamesResponse, MarketApiBetHistoryData, MarketApiBetHistoryResponse, MarketApiCloseMarketData, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiGetUserPointsData, MarketApiGetUserPointsResponse, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiResolveMarketData, MarketApiResolveMarketResponse, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileResponse, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionError, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionError, OpusMagnumApiDeleteSubmissionResponse, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetStatsData, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListResponseFilesData, OpusMagnumApiListResponseFilesError, OpusMagnumApiListResponseFilesResponse, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationError, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoError, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseError, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionError, OpusMagnumApiValidateSubmissionResponse, PlaceApiCanvasData, PlaceApiEvolutionGifData, PlaceApiHeatmapDecayGifData, PlaceApiHeatmapEvolutionGifData, PlaceApiHeatmapPngData, PlaceApiPlacementsCsvData, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheError, PolylanSubmitterApiClearCacheResponse, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponse, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropError, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionResponse, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropError, SubmissionsApiDeleteImageCropResponse, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionError, SubmissionsApiGetSubmissionResponse, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesResponse, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameError, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrError, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrError, SubmissionsApiProcessFileOcrResponse, SubmissionsApiStopSyncScopesData, SubmissionsApiStopSyncScopesError, SubmissionsApiStopSyncScopesResponse, SubmissionsApiSyncScopesData, SubmissionsApiSyncScopesError, SubmissionsApiSyncScopesResponse, SubmissionsApiSyncScopesStatusData, SubmissionsApiSyncScopesStatusError, SubmissionsApiSyncScopesStatusResponse } from '../types.gen';
|
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponse, AnimationsApiResultsData, AnimationsApiResultsResponse, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponse, ClockApiCurrentStateData, ClockApiCurrentStateResponse, ClockApiMyStatsData, ClockApiMyStatsResponse, GamesApiListEventGamesData, GamesApiListEventGamesResponse, GamesApiListEventPlanningData, GamesApiListEventPlanningResponse, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponse, GamesApiListGamesData, GamesApiListGamesResponse, MarketApiBetHistoryData, MarketApiBetHistoryResponse, MarketApiCloseMarketData, MarketApiCreateBetData, MarketApiCreateBetResponse, MarketApiGetUserPointsData, MarketApiGetUserPointsResponse, MarketApiListMarketsData, MarketApiListMarketsResponse, MarketApiListUserBetsData, MarketApiListUserBetsResponse, MarketApiResolveMarketData, MarketApiResolveMarketResponse, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponse, NoitaApiGetResultsData, NoitaApiGetResultsResponse, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileError, NoitaApiSubmitLogFileResponse, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionError, OpusMagnumApiCreateSubmissionResponse, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionError, OpusMagnumApiDeleteSubmissionResponse, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponse, OpusMagnumApiGetStatsData, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponse, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponse, OpusMagnumApiListResponseFilesData, OpusMagnumApiListResponseFilesError, OpusMagnumApiListResponseFilesResponse, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationError, OpusMagnumApiListResponsesNeedingValidationResponse, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponse, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoError, OpusMagnumApiValidateAutoResponse, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseError, OpusMagnumApiValidateResponseResponse, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionError, OpusMagnumApiValidateSubmissionResponse, PlaceApiCanvasData, PlaceApiEvolutionGifData, PlaceApiHeatmapDecayGifData, PlaceApiHeatmapEvolutionGifData, PlaceApiHeatmapPngData, PlaceApiPlacementsCsvData, PlaceApiStatsData, PlaceApiStatsResponse, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheError, PolylanSubmitterApiClearCacheResponse, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponse, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponse, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropError, SubmissionsApiCreateImageCropResponse, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionError, SubmissionsApiCreateSubmissionResponse, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropError, SubmissionsApiDeleteImageCropResponse, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionError, SubmissionsApiGetSubmissionResponse, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesError, SubmissionsApiListAvailableScopesResponse, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesError, SubmissionsApiListMyScopesResponse, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameError, SubmissionsApiListSubmissionsByGameResponse, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsError, SubmissionsApiListUserSubmissionsResponse, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrError, SubmissionsApiProcessCropOcrResponse, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrError, SubmissionsApiProcessFileOcrResponse, SubmissionsApiStopSyncScopesData, SubmissionsApiStopSyncScopesError, SubmissionsApiStopSyncScopesResponse, SubmissionsApiSyncScopesData, SubmissionsApiSyncScopesError, SubmissionsApiSyncScopesResponse, SubmissionsApiSyncScopesStatusData, SubmissionsApiSyncScopesStatusError, SubmissionsApiSyncScopesStatusResponse } from '../types.gen';
|
||||||
|
|
||||||
export type QueryKey<TOptions extends Options> = [
|
export type QueryKey<TOptions extends Options> = [
|
||||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||||
@@ -987,6 +987,27 @@ export const placeApiHeatmapDecayGifOptions = (options: Options<PlaceApiHeatmapD
|
|||||||
queryKey: placeApiHeatmapDecayGifQueryKey(options)
|
queryKey: placeApiHeatmapDecayGifQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const placeApiStatsQueryKey = (options: Options<PlaceApiStatsData>) => createQueryKey('placeApiStats', options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats
|
||||||
|
*
|
||||||
|
* Admin-only: aggregated stats about the game - totals, coverage, top
|
||||||
|
* players, most contested pixels, color usage and activity per hour.
|
||||||
|
*/
|
||||||
|
export const placeApiStatsOptions = (options: Options<PlaceApiStatsData>) => queryOptions<PlaceApiStatsResponse, DefaultError, PlaceApiStatsResponse, ReturnType<typeof placeApiStatsQueryKey>>({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await placeApiStats({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: placeApiStatsQueryKey(options)
|
||||||
|
});
|
||||||
|
|
||||||
export const placeApiPlacementsCsvQueryKey = (options: Options<PlaceApiPlacementsCsvData>) => createQueryKey('placeApiPlacementsCsv', options);
|
export const placeApiPlacementsCsvQueryKey = (options: Options<PlaceApiPlacementsCsvData>) => createQueryKey('placeApiPlacementsCsv', options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
|
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
import { client } from './client.gen';
|
||||||
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, ClockApiCurrentStateData, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListEventPlanningData, GamesApiListEventPlanningResponses, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiBetHistoryData, MarketApiBetHistoryResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionErrors, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionErrors, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponseFilesData, OpusMagnumApiListResponseFilesErrors, OpusMagnumApiListResponseFilesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationErrors, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoErrors, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseErrors, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionErrors, OpusMagnumApiValidateSubmissionResponses, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PlaceApiHeatmapDecayGifData, PlaceApiHeatmapDecayGifResponses, PlaceApiHeatmapEvolutionGifData, PlaceApiHeatmapEvolutionGifResponses, PlaceApiHeatmapPngData, PlaceApiHeatmapPngResponses, PlaceApiPlacementsCsvData, PlaceApiPlacementsCsvResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheErrors, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropErrors, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropErrors, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionErrors, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameErrors, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrErrors, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrErrors, SubmissionsApiProcessFileOcrResponses, SubmissionsApiStopSyncScopesData, SubmissionsApiStopSyncScopesErrors, SubmissionsApiStopSyncScopesResponses, SubmissionsApiSyncScopesData, SubmissionsApiSyncScopesErrors, SubmissionsApiSyncScopesResponses, SubmissionsApiSyncScopesStatusData, SubmissionsApiSyncScopesStatusErrors, SubmissionsApiSyncScopesStatusResponses } from './types.gen';
|
import type { AnimationsApiPuzzleResultsData, AnimationsApiPuzzleResultsResponses, AnimationsApiResultsData, AnimationsApiResultsResponses, AnimationsApiTopSubmissionsData, AnimationsApiTopSubmissionsResponses, ClockApiCurrentStateData, ClockApiCurrentStateResponses, ClockApiMyStatsData, ClockApiMyStatsResponses, GamesApiListEventGamesData, GamesApiListEventGamesResponses, GamesApiListEventPlanningData, GamesApiListEventPlanningResponses, GamesApiListEventSteamGamesData, GamesApiListEventSteamGamesResponses, GamesApiListGamesData, GamesApiListGamesResponses, MarketApiBetHistoryData, MarketApiBetHistoryResponses, MarketApiCloseMarketData, MarketApiCloseMarketResponses, MarketApiCreateBetData, MarketApiCreateBetResponses, MarketApiGetUserPointsData, MarketApiGetUserPointsResponses, MarketApiListMarketsData, MarketApiListMarketsResponses, MarketApiListUserBetsData, MarketApiListUserBetsResponses, MarketApiResolveMarketData, MarketApiResolveMarketResponses, NoitaApiGetLeaderboardData, NoitaApiGetLeaderboardResponses, NoitaApiGetResultsData, NoitaApiGetResultsResponses, NoitaApiSubmitLogFileData, NoitaApiSubmitLogFileErrors, NoitaApiSubmitLogFileResponses, OpusMagnumApiCreateSubmissionData, OpusMagnumApiCreateSubmissionErrors, OpusMagnumApiCreateSubmissionResponses, OpusMagnumApiDeleteSubmissionData, OpusMagnumApiDeleteSubmissionErrors, OpusMagnumApiDeleteSubmissionResponses, OpusMagnumApiGetCollectionData, OpusMagnumApiGetCollectionResponses, OpusMagnumApiGetStatsData, OpusMagnumApiGetStatsResponses, OpusMagnumApiGetSubmissionData, OpusMagnumApiGetSubmissionResponses, OpusMagnumApiListPuzzlesData, OpusMagnumApiListPuzzlesResponses, OpusMagnumApiListResponseFilesData, OpusMagnumApiListResponseFilesErrors, OpusMagnumApiListResponseFilesResponses, OpusMagnumApiListResponsesNeedingValidationData, OpusMagnumApiListResponsesNeedingValidationErrors, OpusMagnumApiListResponsesNeedingValidationResponses, OpusMagnumApiListSubmissionsData, OpusMagnumApiListSubmissionsResponses, OpusMagnumApiValidateAutoData, OpusMagnumApiValidateAutoErrors, OpusMagnumApiValidateAutoResponses, OpusMagnumApiValidateResponseData, OpusMagnumApiValidateResponseErrors, OpusMagnumApiValidateResponseResponses, OpusMagnumApiValidateSubmissionData, OpusMagnumApiValidateSubmissionErrors, OpusMagnumApiValidateSubmissionResponses, PlaceApiCanvasData, PlaceApiCanvasResponses, PlaceApiEvolutionGifData, PlaceApiEvolutionGifResponses, PlaceApiHeatmapDecayGifData, PlaceApiHeatmapDecayGifResponses, PlaceApiHeatmapEvolutionGifData, PlaceApiHeatmapEvolutionGifResponses, PlaceApiHeatmapPngData, PlaceApiHeatmapPngResponses, PlaceApiPlacementsCsvData, PlaceApiPlacementsCsvResponses, PlaceApiStatsData, PlaceApiStatsResponses, PolylanSubmitterApiClearCacheData, PolylanSubmitterApiClearCacheErrors, PolylanSubmitterApiClearCacheResponses, PolylanSubmitterApiGetUserInfoData, PolylanSubmitterApiGetUserInfoResponses, PolylanSubmitterApiHealthCheckData, PolylanSubmitterApiHealthCheckResponses, SubmissionsApiCreateImageCropData, SubmissionsApiCreateImageCropErrors, SubmissionsApiCreateImageCropResponses, SubmissionsApiCreateSubmissionData, SubmissionsApiCreateSubmissionErrors, SubmissionsApiCreateSubmissionResponses, SubmissionsApiDeleteImageCropData, SubmissionsApiDeleteImageCropErrors, SubmissionsApiDeleteImageCropResponses, SubmissionsApiGetSubmissionData, SubmissionsApiGetSubmissionErrors, SubmissionsApiGetSubmissionResponses, SubmissionsApiListAvailableScopesData, SubmissionsApiListAvailableScopesErrors, SubmissionsApiListAvailableScopesResponses, SubmissionsApiListMyScopesData, SubmissionsApiListMyScopesErrors, SubmissionsApiListMyScopesResponses, SubmissionsApiListSubmissionsByGameData, SubmissionsApiListSubmissionsByGameErrors, SubmissionsApiListSubmissionsByGameResponses, SubmissionsApiListUserSubmissionsData, SubmissionsApiListUserSubmissionsErrors, SubmissionsApiListUserSubmissionsResponses, SubmissionsApiProcessCropOcrData, SubmissionsApiProcessCropOcrErrors, SubmissionsApiProcessCropOcrResponses, SubmissionsApiProcessFileOcrData, SubmissionsApiProcessFileOcrErrors, SubmissionsApiProcessFileOcrResponses, SubmissionsApiStopSyncScopesData, SubmissionsApiStopSyncScopesErrors, SubmissionsApiStopSyncScopesResponses, SubmissionsApiSyncScopesData, SubmissionsApiSyncScopesErrors, SubmissionsApiSyncScopesResponses, SubmissionsApiSyncScopesStatusData, SubmissionsApiSyncScopesStatusErrors, SubmissionsApiSyncScopesStatusResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
||||||
/**
|
/**
|
||||||
@@ -414,6 +414,22 @@ export const placeApiHeatmapDecayGif = <ThrowOnError extends boolean = false>(op
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats
|
||||||
|
*
|
||||||
|
* Admin-only: aggregated stats about the game - totals, coverage, top
|
||||||
|
* players, most contested pixels, color usage and activity per hour.
|
||||||
|
*/
|
||||||
|
export const placeApiStats = <ThrowOnError extends boolean = false>(options: Options<PlaceApiStatsData, ThrowOnError>) => (options.client ?? client).get<PlaceApiStatsResponses, unknown, ThrowOnError>({
|
||||||
|
security: [{
|
||||||
|
in: 'cookie',
|
||||||
|
name: 'sessionid',
|
||||||
|
type: 'apiKey'
|
||||||
|
}],
|
||||||
|
url: '/api/events/{event_path}/place/stats',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placements Csv
|
* Placements Csv
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1264,6 +1264,144 @@ export type PlayerStatsOut = {
|
|||||||
points_below: number | null;
|
points_below: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ColorCountOut
|
||||||
|
*/
|
||||||
|
export type ColorCountOut = {
|
||||||
|
/**
|
||||||
|
* Color Index
|
||||||
|
*/
|
||||||
|
color_index: number;
|
||||||
|
/**
|
||||||
|
* Color
|
||||||
|
*/
|
||||||
|
color: string;
|
||||||
|
/**
|
||||||
|
* Placements
|
||||||
|
*/
|
||||||
|
placements: number;
|
||||||
|
/**
|
||||||
|
* On Canvas
|
||||||
|
*/
|
||||||
|
on_canvas: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HourCountOut
|
||||||
|
*/
|
||||||
|
export type HourCountOut = {
|
||||||
|
/**
|
||||||
|
* Hour
|
||||||
|
*/
|
||||||
|
hour: string;
|
||||||
|
/**
|
||||||
|
* Placements
|
||||||
|
*/
|
||||||
|
placements: number;
|
||||||
|
/**
|
||||||
|
* Players
|
||||||
|
*/
|
||||||
|
players: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PixelCountOut
|
||||||
|
*/
|
||||||
|
export type PixelCountOut = {
|
||||||
|
/**
|
||||||
|
* X
|
||||||
|
*/
|
||||||
|
x: number;
|
||||||
|
/**
|
||||||
|
* Y
|
||||||
|
*/
|
||||||
|
y: number;
|
||||||
|
/**
|
||||||
|
* Placements
|
||||||
|
*/
|
||||||
|
placements: number;
|
||||||
|
/**
|
||||||
|
* Players
|
||||||
|
*/
|
||||||
|
players: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlaceStatsOut
|
||||||
|
*/
|
||||||
|
export type PlaceStatsOut = {
|
||||||
|
/**
|
||||||
|
* Width
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
/**
|
||||||
|
* Total Placements
|
||||||
|
*/
|
||||||
|
total_placements: number;
|
||||||
|
/**
|
||||||
|
* Total Players
|
||||||
|
*/
|
||||||
|
total_players: number;
|
||||||
|
/**
|
||||||
|
* First Placement At
|
||||||
|
*/
|
||||||
|
first_placement_at: string | null;
|
||||||
|
/**
|
||||||
|
* Last Placement At
|
||||||
|
*/
|
||||||
|
last_placement_at: string | null;
|
||||||
|
/**
|
||||||
|
* Pixels Touched
|
||||||
|
*/
|
||||||
|
pixels_touched: number;
|
||||||
|
/**
|
||||||
|
* Coverage
|
||||||
|
*/
|
||||||
|
coverage: number;
|
||||||
|
/**
|
||||||
|
* Overwrites
|
||||||
|
*/
|
||||||
|
overwrites: number;
|
||||||
|
/**
|
||||||
|
* Avg Placements Per Player
|
||||||
|
*/
|
||||||
|
avg_placements_per_player: number;
|
||||||
|
/**
|
||||||
|
* Top Players
|
||||||
|
*/
|
||||||
|
top_players: Array<PlayerCountOut>;
|
||||||
|
/**
|
||||||
|
* Most Contested Pixels
|
||||||
|
*/
|
||||||
|
most_contested_pixels: Array<PixelCountOut>;
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*/
|
||||||
|
colors: Array<ColorCountOut>;
|
||||||
|
/**
|
||||||
|
* Per Hour
|
||||||
|
*/
|
||||||
|
per_hour: Array<HourCountOut>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlayerCountOut
|
||||||
|
*/
|
||||||
|
export type PlayerCountOut = {
|
||||||
|
/**
|
||||||
|
* Username
|
||||||
|
*/
|
||||||
|
username: string;
|
||||||
|
/**
|
||||||
|
* Placements
|
||||||
|
*/
|
||||||
|
placements: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MarketListSchema
|
* MarketListSchema
|
||||||
*/
|
*/
|
||||||
@@ -2823,6 +2961,27 @@ export type PlaceApiHeatmapDecayGifResponses = {
|
|||||||
200: unknown;
|
200: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PlaceApiStatsData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Event Path
|
||||||
|
*/
|
||||||
|
event_path: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/api/events/{event_path}/place/stats';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaceApiStatsResponses = {
|
||||||
|
/**
|
||||||
|
* OK
|
||||||
|
*/
|
||||||
|
200: PlaceStatsOut;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlaceApiStatsResponse = PlaceApiStatsResponses[keyof PlaceApiStatsResponses];
|
||||||
|
|
||||||
export type PlaceApiPlacementsCsvData = {
|
export type PlaceApiPlacementsCsvData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const gridHeight = ref(DEFAULT_GRID_SIZE)
|
|||||||
export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?: OnResize) {
|
export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?: OnResize) {
|
||||||
const cooldownEndMs = ref<number | null>(null)
|
const cooldownEndMs = ref<number | null>(null)
|
||||||
const cooldownRemainingMs = ref(0)
|
const cooldownRemainingMs = ref(0)
|
||||||
|
// An admin can freeze the board (PlaceSettings.placement_enabled) - the
|
||||||
|
// server rejects placements anyway, this just lets the UI say so.
|
||||||
|
const placementEnabled = ref(true)
|
||||||
|
|
||||||
let animationFrame: number | null = null
|
let animationFrame: number | null = null
|
||||||
|
|
||||||
@@ -65,9 +68,10 @@ export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?:
|
|||||||
|
|
||||||
switch (parts[0]) {
|
switch (parts[0]) {
|
||||||
case 's': {
|
case 's': {
|
||||||
const [, next, width, height] = parts
|
const [, next, width, height, enabled] = parts
|
||||||
applyCooldown(next !== '' ? Number(next) : null)
|
applyCooldown(next !== '' ? Number(next) : null)
|
||||||
applyGridSize(Number(width), Number(height))
|
applyGridSize(Number(width), Number(height))
|
||||||
|
placementEnabled.value = enabled === '1'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'p': {
|
case 'p': {
|
||||||
@@ -81,10 +85,12 @@ export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?:
|
|||||||
}
|
}
|
||||||
case 'r': {
|
case 'r': {
|
||||||
if (parts[1] === 'c') applyCooldown(Number(parts[2]))
|
if (parts[1] === 'c') applyCooldown(Number(parts[2]))
|
||||||
|
if (parts[1] === 'd') placementEnabled.value = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'e': {
|
case 'e': {
|
||||||
const [, width, height] = parts
|
const [, width, height, enabled] = parts
|
||||||
|
placementEnabled.value = enabled === '1'
|
||||||
// The server already rebuilt its canvas at the new size (see
|
// The server already rebuilt its canvas at the new size (see
|
||||||
// place/signals.py) - just re-fetch it rather than guess how to
|
// place/signals.py) - just re-fetch it rather than guess how to
|
||||||
// remap our local pixels.
|
// remap our local pixels.
|
||||||
@@ -151,6 +157,7 @@ export function usePlaceSocket(eventPath: string, onPixel?: OnPixel, onResize?:
|
|||||||
return {
|
return {
|
||||||
connected,
|
connected,
|
||||||
cooldownRemainingMs,
|
cooldownRemainingMs,
|
||||||
|
placementEnabled,
|
||||||
cooldownDurationSeconds: PLACE_COOLDOWN_SECONDS,
|
cooldownDurationSeconds: PLACE_COOLDOWN_SECONDS,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight,
|
gridHeight,
|
||||||
|
|||||||
@@ -97,7 +97,8 @@
|
|||||||
"pageTitle": "Place",
|
"pageTitle": "Place",
|
||||||
"header": "Place",
|
"header": "Place",
|
||||||
"showGrid": "Show grid",
|
"showGrid": "Show grid",
|
||||||
"hideGrid": "Hide grid"
|
"hideGrid": "Hide grid",
|
||||||
|
"placementDisabled": "Pixel placement is paused"
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
"pageTitle": "Market",
|
"pageTitle": "Market",
|
||||||
|
|||||||
Reference in New Issue
Block a user