basic noita parsing
This commit is contained in:
parent
119fdc2a51
commit
69b6b46ee2
@ -1 +1,36 @@
|
|||||||
# Register your models here.
|
from django.contrib import admin
|
||||||
|
from .models import LogfileSubmission, Objectiv, ObjectivPoint
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(LogfileSubmission)
|
||||||
|
class LogfileSubmissionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("id", "user", "content_type", "file_size", "created_at", "processed")
|
||||||
|
list_filter = ("content_type", "processed", "created_at")
|
||||||
|
search_fields = ("id", "user__username")
|
||||||
|
readonly_fields = ("id", "created_at", "updated_at")
|
||||||
|
fieldsets = (
|
||||||
|
("Identification", {"fields": ("id",)}),
|
||||||
|
("File Information", {"fields": ("file", "content_type", "file_size")}),
|
||||||
|
("User", {"fields": ("user",)}),
|
||||||
|
("Timestamps", {"fields": ("created_at", "updated_at")}),
|
||||||
|
("Processing", {"fields": ("processed",)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Objectiv)
|
||||||
|
class ObjectivAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("objectiv_id", "user", "count")
|
||||||
|
list_filter = ("objectiv_id", "user")
|
||||||
|
search_fields = ("objectiv_id", "user__username")
|
||||||
|
readonly_fields = ("user",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ObjectivPoint)
|
||||||
|
class ObjectivPointAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("objectiv_id", "display_string", "max_count", "point")
|
||||||
|
list_filter = ("objectiv_id",)
|
||||||
|
search_fields = ("objectiv_id", "display_string")
|
||||||
|
fieldsets = (
|
||||||
|
("Objective Information", {"fields": ("objectiv_id", "display_string")}),
|
||||||
|
("Scoring", {"fields": ("max_count", "point")}),
|
||||||
|
)
|
||||||
|
|||||||
@ -3,13 +3,20 @@ from django.core.files.base import ContentFile
|
|||||||
from ninja import Router, File
|
from ninja import Router, File
|
||||||
from ninja.files import UploadedFile
|
from ninja.files import UploadedFile
|
||||||
|
|
||||||
from .models import LogfileSubmission
|
from noita.schemas import ObjectivOut
|
||||||
|
|
||||||
|
from .models import LogfileSubmission, Objectiv
|
||||||
from .schemas import NoitaSubmissionOut
|
from .schemas import NoitaSubmissionOut
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("objectives", response=list[ObjectivOut])
|
||||||
|
def get_my_objectives(request: HttpRequest):
|
||||||
|
return Objectiv.objects.order_by("-count").filter(user=request.user)
|
||||||
|
|
||||||
|
|
||||||
@router.post("submit", response=NoitaSubmissionOut)
|
@router.post("submit", response=NoitaSubmissionOut)
|
||||||
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||||
"""
|
"""
|
||||||
|
|||||||
38
polylan_submitter/noita/migrations/0003_objectiv.py
Normal file
38
polylan_submitter/noita/migrations/0003_objectiv.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-09 23:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("noita", "0002_rename_submission_logfilesubmission"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Objectiv",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("updated_at", models.DateTimeField()),
|
||||||
|
("objectiv_id", models.CharField(max_length=64)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
17
polylan_submitter/noita/migrations/0004_objectiv_count.py
Normal file
17
polylan_submitter/noita/migrations/0004_objectiv_count.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-09 23:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("noita", "0003_objectiv"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="objectiv",
|
||||||
|
name="count",
|
||||||
|
field=models.IntegerField(default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-09 23:21
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("noita", "0004_objectiv_count"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="objectiv",
|
||||||
|
name="updated_at",
|
||||||
|
),
|
||||||
|
]
|
||||||
30
polylan_submitter/noita/migrations/0006_objectivpoint.py
Normal file
30
polylan_submitter/noita/migrations/0006_objectivpoint.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-09 23:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("noita", "0005_remove_objectiv_updated_at"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ObjectivPoint",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("objectiv_id", models.CharField(max_length=64, unique=True)),
|
||||||
|
("display_string", models.CharField(max_length=255)),
|
||||||
|
("max_count", models.IntegerField(default=1)),
|
||||||
|
("point", models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -42,3 +42,18 @@ class LogfileSubmission(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
processed = models.BooleanField(default=False)
|
processed = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Objectiv(models.Model):
|
||||||
|
objectiv_id = models.CharField(max_length=64)
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
count = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectivPoint(models.Model):
|
||||||
|
objectiv_id = models.CharField(max_length=64, unique=True)
|
||||||
|
display_string = models.CharField(max_length=255)
|
||||||
|
max_count = models.IntegerField(default=1)
|
||||||
|
point = models.IntegerField(default=0)
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from ninja import Schema, ModelSchema
|
||||||
|
|
||||||
|
from noita.models import Objectiv
|
||||||
|
|
||||||
|
|
||||||
class NoitaSubmissionOut(BaseModel):
|
class ObjectivOut(ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = Objectiv
|
||||||
|
fields = ["objectiv_id", "count"]
|
||||||
|
|
||||||
|
|
||||||
|
class NoitaSubmissionOut(Schema):
|
||||||
id: str
|
id: str
|
||||||
user_id: Optional[int]
|
user_id: Optional[int]
|
||||||
username: Optional[str]
|
username: Optional[str]
|
||||||
@ -11,6 +19,3 @@ class NoitaSubmissionOut(BaseModel):
|
|||||||
content_type: str
|
content_type: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
processed: bool
|
processed: bool
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|||||||
0
polylan_submitter/noita/services/__init__.py
Normal file
0
polylan_submitter/noita/services/__init__.py
Normal file
291
polylan_submitter/noita/services/decode.py
Normal file
291
polylan_submitter/noita/services/decode.py
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
"""
|
||||||
|
Decode a polylan_mod_log.txt.
|
||||||
|
|
||||||
|
Hash format: sha1(seed|timestamp|id) — timestamp is bound into the hash so
|
||||||
|
it cannot be altered without breaking the signature.
|
||||||
|
|
||||||
|
Seed-less entries (INIT, DEBUG, mod checks) use sha1(id) with no seed or
|
||||||
|
timestamp — those are resolved via a static lookup.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python decode_log.py [path/to/polylan_mod_log.txt]
|
||||||
|
|
||||||
|
Default path: ~/.local/share/Steam/steamapps/common/Noita/polylan_mod_log.txt
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
from functools import cache
|
||||||
|
from noita.services.spells import ALL_SPELLS, ALL_PERKS
|
||||||
|
|
||||||
|
|
||||||
|
SEED_POOL = [
|
||||||
|
# General good seeds
|
||||||
|
3154823,
|
||||||
|
3718311,
|
||||||
|
10064758,
|
||||||
|
123156801,
|
||||||
|
1024089369,
|
||||||
|
1026967166,
|
||||||
|
# Pacifist seeds
|
||||||
|
177795258,
|
||||||
|
520542929,
|
||||||
|
10600249,
|
||||||
|
25300740,
|
||||||
|
21875589,
|
||||||
|
24085389,
|
||||||
|
59775105,
|
||||||
|
44190726,
|
||||||
|
1039649471,
|
||||||
|
1072607354,
|
||||||
|
# Perk combo seeds
|
||||||
|
839747651,
|
||||||
|
839844768,
|
||||||
|
840909713,
|
||||||
|
839959129,
|
||||||
|
840016192,
|
||||||
|
840039886,
|
||||||
|
840398606,
|
||||||
|
840439045,
|
||||||
|
840457463,
|
||||||
|
840492754,
|
||||||
|
840507802,
|
||||||
|
840513742,
|
||||||
|
840542079,
|
||||||
|
840574169,
|
||||||
|
840610974,
|
||||||
|
840626894,
|
||||||
|
840872436,
|
||||||
|
840894605,
|
||||||
|
841221188,
|
||||||
|
]
|
||||||
|
|
||||||
|
DEATH_PENALTY = 1
|
||||||
|
|
||||||
|
# All scoreable events: base 1 pt for every spell and perk, overrides below.
|
||||||
|
POINTS = {
|
||||||
|
# ── Spells ───────────────────────────────────────────────────────────────
|
||||||
|
**{sid: 1 for sid in ALL_SPELLS},
|
||||||
|
"ADD_TRIGGER": 10,
|
||||||
|
"NOLLA": 10,
|
||||||
|
"CHAOTIC_TRANSMUTATION": 10,
|
||||||
|
"DUPLICATE": 5,
|
||||||
|
"OMEGA": 10,
|
||||||
|
"HEALING_BOLT": 5,
|
||||||
|
# ── Perks ─────────────────────────────────────────────────────────────────
|
||||||
|
**{pid: 1 for pid in ALL_PERKS},
|
||||||
|
"EDIT_WANDS_EVERYWHERE": 10,
|
||||||
|
# ── Spell combos ─────────────────────────────────────────────────────────
|
||||||
|
"PING_PONG_DRILL": 10,
|
||||||
|
"HEAVY_SHOT_DISC": 10,
|
||||||
|
"TOUCH_OF_ANY": 5,
|
||||||
|
"TWO_ARC_MODIFIERS": 10,
|
||||||
|
"TWO_TRAIL_MODIFIERS": 10,
|
||||||
|
# ── Perk combos ──────────────────────────────────────────────────────────
|
||||||
|
"CRIMSON_ALCHEMIST": 15,
|
||||||
|
"GREEDY_GOBLIN_KING": 15,
|
||||||
|
"STORM_TOUCHED_ASCENDANT": 15,
|
||||||
|
"ARCHMAGE_OF_CONTROL": 15,
|
||||||
|
"HAUNTED_MAGE": 15,
|
||||||
|
"GLASS_CANNON_MESSIAH": 15,
|
||||||
|
"INFINITE_ENGINE": 15,
|
||||||
|
"PERFECT_ACCURACY_LOOP": 15,
|
||||||
|
"HOMING_DEATH_SWARM": 15,
|
||||||
|
"UNTOUCHABLE_FIELD": 15,
|
||||||
|
"ELECTRIC_SUSTAIN_LOOP": 15,
|
||||||
|
"IMMORTAL_LEECH_CORE": 15,
|
||||||
|
"PROJECTILE_OVERLOAD": 15,
|
||||||
|
"CLOSE_RANGE_DEATH_MACHINE": 15,
|
||||||
|
"CRITICAL_MASS": 15,
|
||||||
|
"HOLY_MOUNTAIN_ABUSER": 15,
|
||||||
|
"DEFLECTOR_MATRIX": 15,
|
||||||
|
"STORMBORNE_LEVITATOR": 15,
|
||||||
|
# ── Objectives ───────────────────────────────────────────────────────────
|
||||||
|
"HP_200": 5,
|
||||||
|
"HP_500": 5,
|
||||||
|
"HP_2000": 5,
|
||||||
|
"HP_5000": 5,
|
||||||
|
"GOLD_1000": 5,
|
||||||
|
"GOLD_10000": 5,
|
||||||
|
"GOLD_100000": 5,
|
||||||
|
"GOLD_1000000": 5,
|
||||||
|
**{f"ORB_{i}": 5 for i in range(34)},
|
||||||
|
"FIND_AMBROSIA": 10,
|
||||||
|
"WAND_MANA_500": 10,
|
||||||
|
"WAND_MANA_1000": 10,
|
||||||
|
"WAND_MANA_1500": 10,
|
||||||
|
"WAND_CAPACITY_10": 10,
|
||||||
|
"WAND_CAPACITY_20": 10,
|
||||||
|
"BOSS_KILL": 100,
|
||||||
|
"CRIMSON_ALCHEMIST-BOSS_KILL": 100,
|
||||||
|
"GREEDY_GOBLIN_KING-BOSS_KILL": 100,
|
||||||
|
"STORM_TOUCHED_ASCENDANT-BOSS_KILL": 100,
|
||||||
|
"ARCHMAGE_OF_CONTROL-BOSS_KILL": 100,
|
||||||
|
"HAUNTED_MAGE-BOSS_KILL": 100,
|
||||||
|
"GLASS_CANNON_MESSIAH-BOSS_KILL": 100,
|
||||||
|
"INFINITE_ENGINE-BOSS_KILL": 100,
|
||||||
|
"PERFECT_ACCURACY_LOOP-BOSS_KILL": 100,
|
||||||
|
"HOMING_DEATH_SWARM-BOSS_KILL": 100,
|
||||||
|
"UNTOUCHABLE_FIELD-BOSS_KILL": 100,
|
||||||
|
"ELECTRIC_SUSTAIN_LOOP-BOSS_KILL": 100,
|
||||||
|
"IMMORTAL_LEECH_CORE-BOSS_KILL": 100,
|
||||||
|
"PROJECTILE_OVERLOAD-BOSS_KILL": 100,
|
||||||
|
"CLOSE_RANGE_DEATH_MACHINE-BOSS_KILL": 100,
|
||||||
|
"CRITICAL_MASS-BOSS_KILL": 100,
|
||||||
|
"HOLY_MOUNTAIN_ABUSER-BOSS_KILL": 100,
|
||||||
|
"DEFLECTOR_MATRIX-BOSS_KILL": 100,
|
||||||
|
"STORMBORNE_LEVITATOR-BOSS_KILL": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Perk-combo IDs — used to award a per-combo boss-kill bonus.
|
||||||
|
PERK_COMBO_IDS = {
|
||||||
|
"CRIMSON_ALCHEMIST",
|
||||||
|
"GREEDY_GOBLIN_KING",
|
||||||
|
"STORM_TOUCHED_ASCENDANT",
|
||||||
|
"ARCHMAGE_OF_CONTROL",
|
||||||
|
"HAUNTED_MAGE",
|
||||||
|
"GLASS_CANNON_MESSIAH",
|
||||||
|
"INFINITE_ENGINE",
|
||||||
|
"PERFECT_ACCURACY_LOOP",
|
||||||
|
"HOMING_DEATH_SWARM",
|
||||||
|
"UNTOUCHABLE_FIELD",
|
||||||
|
"ELECTRIC_SUSTAIN_LOOP",
|
||||||
|
"IMMORTAL_LEECH_CORE",
|
||||||
|
"PROJECTILE_OVERLOAD",
|
||||||
|
"CLOSE_RANGE_DEATH_MACHINE",
|
||||||
|
"CRITICAL_MASS",
|
||||||
|
"HOLY_MOUNTAIN_ABUSER",
|
||||||
|
"DEFLECTOR_MATRIX",
|
||||||
|
"STORMBORNE_LEVITATOR",
|
||||||
|
}
|
||||||
|
|
||||||
|
BOSS_KILL_COMBO_BONUS = 100 # default bonus per active perk combo on boss kill
|
||||||
|
|
||||||
|
# IDs tried with the seed+timestamp scheme.
|
||||||
|
_ALL_IDS = list(POINTS.keys()) + ["DEATH"]
|
||||||
|
|
||||||
|
# Seed-less hashes (sha1(id) only, no seed, no timestamp).
|
||||||
|
_STATIC_LOOKUP = {
|
||||||
|
hashlib.sha1(k.encode()).hexdigest(): k
|
||||||
|
for k in ["-", "DEBUG", "polylan-mod", "cheatgui", "alchemy_recipes_display"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sha1(text: str) -> str:
|
||||||
|
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def resolve(
|
||||||
|
hash_val: str, ts: str, preferred_seed: int | None = None
|
||||||
|
) -> tuple[str | None, int | None]:
|
||||||
|
"""Return (name, seed) or (None, None). preferred_seed is tried first."""
|
||||||
|
if hash_val in _STATIC_LOOKUP:
|
||||||
|
return _STATIC_LOOKUP[hash_val], None
|
||||||
|
|
||||||
|
seeds = (
|
||||||
|
[preferred_seed] + [s for s in SEED_POOL if s != preferred_seed]
|
||||||
|
if preferred_seed is not None
|
||||||
|
else SEED_POOL
|
||||||
|
)
|
||||||
|
|
||||||
|
for seed in seeds:
|
||||||
|
prefix = f"{seed}|{ts}|"
|
||||||
|
for name in _ALL_IDS:
|
||||||
|
if _sha1(prefix + name) == hash_val:
|
||||||
|
return name, seed
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log(file) -> list:
|
||||||
|
entries = []
|
||||||
|
for il, line in enumerate(file.split("\n")):
|
||||||
|
m = re.match(r"\[(.+?)\] ([0-9a-f]{40})", line.rstrip())
|
||||||
|
if m:
|
||||||
|
entries.append({"ts": m.group(1), "hash": m.group(2)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Unable to parse line number {il:>4}: {line.strip()}??")
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def decode(file) -> None:
|
||||||
|
entries = parse_log(file)
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
print("No entries found in log.")
|
||||||
|
return
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
total = 0
|
||||||
|
deaths = 0
|
||||||
|
have_init = False
|
||||||
|
have_debug = False
|
||||||
|
known_seed = None # cached once first seeded entry is resolved
|
||||||
|
current_run_perk_combos = set() # perk combos seen since the last "-" entry
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
name, seed = resolve(entry["hash"], entry["ts"], known_seed)
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
print(f" ???? (unresolved) {entry['hash']} ts: {entry['ts']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if seed is not None and known_seed is None:
|
||||||
|
known_seed = seed
|
||||||
|
|
||||||
|
if name == "-":
|
||||||
|
have_init = True
|
||||||
|
current_run_perk_combos = set()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name == "DEBUG":
|
||||||
|
have_debug = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name == "DEATH":
|
||||||
|
deaths += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip mod-presence entries
|
||||||
|
if name not in POINTS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name in seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name not in {"BOSS_KILL"}:
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
if name in PERK_COMBO_IDS:
|
||||||
|
current_run_perk_combos.add(name)
|
||||||
|
|
||||||
|
pts = POINTS.get(name, 0)
|
||||||
|
if pts:
|
||||||
|
print(f" +{pts:>4} {name:<40} first seen: {entry['ts']}")
|
||||||
|
total += pts
|
||||||
|
|
||||||
|
if name == "BOSS_KILL":
|
||||||
|
for combo_id in sorted(current_run_perk_combos):
|
||||||
|
bonus_key = f"{combo_id}-BOSS_KILL"
|
||||||
|
if bonus_key not in seen:
|
||||||
|
seen.add(bonus_key)
|
||||||
|
bonus_pts = POINTS.get(bonus_key, BOSS_KILL_COMBO_BONUS)
|
||||||
|
print(f" +{bonus_pts:>4} {bonus_key:<40} combo bonus")
|
||||||
|
total += bonus_pts
|
||||||
|
|
||||||
|
death_deduction = deaths * DEATH_PENALTY
|
||||||
|
if deaths:
|
||||||
|
print(
|
||||||
|
f" -{death_deduction:>4} ({deaths} death{'s' if deaths > 1 else ''} × {DEATH_PENALTY} pts)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if have_init:
|
||||||
|
print(
|
||||||
|
f"\nTotal: {total - death_deduction} pts ({total} earned − {death_deduction} penalty)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if have_debug:
|
||||||
|
print("Note: DEBUG mode was active during at least one session.")
|
||||||
40
polylan_submitter/noita/services/objectives.py
Normal file
40
polylan_submitter/noita/services/objectives.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from noita.models import LogfileSubmission, Objectiv
|
||||||
|
from noita.services.decode import parse_log, resolve
|
||||||
|
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
|
def parse_objectives_from_logfile(logfile: LogfileSubmission) -> Counter:
|
||||||
|
"""Parse a log file, and output a count for each ID."""
|
||||||
|
file_data = logfile.file.read().decode()
|
||||||
|
|
||||||
|
ids = []
|
||||||
|
for entry in parse_log(file_data):
|
||||||
|
idx, _seed = resolve(entry["hash"], entry["ts"])
|
||||||
|
|
||||||
|
if idx:
|
||||||
|
ids.append(idx)
|
||||||
|
|
||||||
|
return Counter(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_objectives_and_store(logfile: LogfileSubmission) -> None:
|
||||||
|
"""Parse a logfile and store output."""
|
||||||
|
|
||||||
|
if not logfile.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
counter = parse_objectives_from_logfile(logfile)
|
||||||
|
|
||||||
|
for idx, count in counter.items():
|
||||||
|
if idx in {"-", "DEBUG"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj, created = Objectiv.objects.get_or_create(
|
||||||
|
objectiv_id=idx,
|
||||||
|
user=logfile.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
obj.count += count
|
||||||
|
obj.save(update_fields=["count"])
|
||||||
535
polylan_submitter/noita/services/spells.py
Normal file
535
polylan_submitter/noita/services/spells.py
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
ALL_SPELLS = [
|
||||||
|
"FUNKY_SPELL",
|
||||||
|
"ACIDSHOT",
|
||||||
|
"BLACK_HOLE",
|
||||||
|
"BLACK_HOLE_DEATH_TRIGGER",
|
||||||
|
"BOMB",
|
||||||
|
"BOMB_CART",
|
||||||
|
"BUBBLESHOT",
|
||||||
|
"BUBBLESHOT_TRIGGER",
|
||||||
|
"AIR_BULLET",
|
||||||
|
"CHAIN_BOLT",
|
||||||
|
"CHAINSAW",
|
||||||
|
"CURSED_ORB",
|
||||||
|
"ANTIHEAL",
|
||||||
|
"DEATH_CROSS",
|
||||||
|
"DEATH_CROSS_BIG",
|
||||||
|
"LASER_EMITTER_FOUR",
|
||||||
|
"POWERDIGGER",
|
||||||
|
"DIGGER",
|
||||||
|
"PIPE_BOMB",
|
||||||
|
"PIPE_BOMB_DEATH_TRIGGER",
|
||||||
|
"GRENADE_LARGE",
|
||||||
|
"DYNAMITE",
|
||||||
|
"CRUMBLING_EARTH",
|
||||||
|
"TENTACLE_PORTAL",
|
||||||
|
"SLOW_BULLET",
|
||||||
|
"SLOW_BULLET_TRIGGER",
|
||||||
|
"SLOW_BULLET_TIMER",
|
||||||
|
"EXPANDING_ORB",
|
||||||
|
"FIREBALL",
|
||||||
|
"GRENADE",
|
||||||
|
"GRENADE_TRIGGER",
|
||||||
|
"GRENADE_TIER_2",
|
||||||
|
"GRENADE_TIER_3",
|
||||||
|
"GRENADE_ANTI",
|
||||||
|
"FIREBOMB",
|
||||||
|
"FIREWORK",
|
||||||
|
"FLAMETHROWER",
|
||||||
|
"GLITTER_BOMB",
|
||||||
|
"LANCE",
|
||||||
|
"GLUE_SHOT",
|
||||||
|
"HEAL_BULLET",
|
||||||
|
"LANCE_HOLY",
|
||||||
|
"BOMB_HOLY",
|
||||||
|
"BOMB_HOLY_GIGA",
|
||||||
|
"HOOK",
|
||||||
|
"ICEBALL",
|
||||||
|
"LASER",
|
||||||
|
"LIGHTNING",
|
||||||
|
"THUNDERBALL",
|
||||||
|
"BALL_LIGHTNING",
|
||||||
|
"LUMINOUS_DRILL",
|
||||||
|
"LASER_LUMINOUS_DRILL",
|
||||||
|
"BULLET",
|
||||||
|
"BULLET_TRIGGER",
|
||||||
|
"BULLET_TIMER",
|
||||||
|
"HEAVY_BULLET",
|
||||||
|
"HEAVY_BULLET_TRIGGER",
|
||||||
|
"HEAVY_BULLET_TIMER",
|
||||||
|
"MAGIC_SHIELD",
|
||||||
|
"BIG_MAGIC_SHIELD",
|
||||||
|
"ROCKET",
|
||||||
|
"ROCKET_TIER_2",
|
||||||
|
"ROCKET_TIER_3",
|
||||||
|
"METEOR",
|
||||||
|
"MIST_BLOOD",
|
||||||
|
"MIST_ALCOHOL",
|
||||||
|
"MIST_SLIME",
|
||||||
|
"MIST_RADIOACTIVE",
|
||||||
|
"BUCKSHOT",
|
||||||
|
"MEGALASER",
|
||||||
|
"EXPLODING_DUCKS",
|
||||||
|
"FREEZING_GAZE",
|
||||||
|
"INFESTATION",
|
||||||
|
"NUKE",
|
||||||
|
"NUKE_GIGA",
|
||||||
|
"DARKFLAME",
|
||||||
|
"GLOWING_BOLT",
|
||||||
|
"LASER_EMITTER",
|
||||||
|
"LASER_EMITTER_CUTTER",
|
||||||
|
"POLLEN",
|
||||||
|
"SPORE_POD",
|
||||||
|
"PROPANE_TANK",
|
||||||
|
"RANDOM_PROJECTILE",
|
||||||
|
"SUMMON_ROCK",
|
||||||
|
"DISC_BULLET",
|
||||||
|
"DISC_BULLET_BIG",
|
||||||
|
"DISC_BULLET_BIGGER",
|
||||||
|
"SLIMEBALL",
|
||||||
|
"LIGHT_BULLET",
|
||||||
|
"LIGHT_BULLET_TRIGGER",
|
||||||
|
"LIGHT_BULLET_TRIGGER_2",
|
||||||
|
"LIGHT_BULLET_TIMER",
|
||||||
|
"RUBBER_BALL",
|
||||||
|
"ARROW",
|
||||||
|
"BOUNCY_ORB",
|
||||||
|
"BOUNCY_ORB_TIMER",
|
||||||
|
"SPIRAL_SHOT",
|
||||||
|
"SPITTER",
|
||||||
|
"SPITTER_TIMER",
|
||||||
|
"SPITTER_TIER_2",
|
||||||
|
"SPITTER_TIER_2_TIMER",
|
||||||
|
"SPITTER_TIER_3",
|
||||||
|
"SPITTER_TIER_3_TIMER",
|
||||||
|
"EXPLODING_DEER",
|
||||||
|
"SUMMON_EGG",
|
||||||
|
"TNTBOX",
|
||||||
|
"TNTBOX_BIG",
|
||||||
|
"FISH",
|
||||||
|
"SUMMON_HOLLOW_EGG",
|
||||||
|
"MISSILE",
|
||||||
|
"PEBBLE",
|
||||||
|
"TENTACLE",
|
||||||
|
"TENTACLE_TIMER",
|
||||||
|
"TELEPORT_PROJECTILE_CLOSER",
|
||||||
|
"TELEPORT_PROJECTILE_STATIC",
|
||||||
|
"SWAPPER_PROJECTILE",
|
||||||
|
"TELEPORT_PROJECTILE",
|
||||||
|
"TELEPORT_PROJECTILE_SHORT",
|
||||||
|
"MINE",
|
||||||
|
"MINE_DEATH_TRIGGER",
|
||||||
|
"WHITE_HOLE",
|
||||||
|
"WORM_SHOT",
|
||||||
|
"WALL_HORIZONTAL",
|
||||||
|
"WALL_VERTICAL",
|
||||||
|
"WALL_SQUARE",
|
||||||
|
"REGENERATION_FIELD",
|
||||||
|
"FREEZE_FIELD",
|
||||||
|
"LEVITATION_FIELD",
|
||||||
|
"TELEPORTATION_FIELD",
|
||||||
|
"BERSERK_FIELD",
|
||||||
|
"SHIELD_FIELD",
|
||||||
|
"ELECTROCUTION_FIELD",
|
||||||
|
"POLYMORPH_FIELD",
|
||||||
|
"CHAOS_POLYMORPH_FIELD",
|
||||||
|
"CLOUD_WATER",
|
||||||
|
"CLOUD_OIL",
|
||||||
|
"CLOUD_BLOOD",
|
||||||
|
"CLOUD_ACID",
|
||||||
|
"CLOUD_THUNDER",
|
||||||
|
"DESTRUCTION",
|
||||||
|
"BOMB_DETONATOR",
|
||||||
|
"PURPLE_EXPLOSION_FIELD",
|
||||||
|
"WORM_RAIN",
|
||||||
|
"METEOR_RAIN",
|
||||||
|
"SWARM_FLY",
|
||||||
|
"SWARM_FIREBUG",
|
||||||
|
"SWARM_WASP",
|
||||||
|
"DELAYED_SPELL",
|
||||||
|
"MASS_POLYMORPH",
|
||||||
|
"PROJECTILE_THUNDER_FIELD",
|
||||||
|
"PROJECTILE_GRAVITY_FIELD",
|
||||||
|
"PROJECTILE_TRANSMUTATION_FIELD",
|
||||||
|
"RANDOM_STATIC_PROJECTILE",
|
||||||
|
"WHITE_HOLE_BIG",
|
||||||
|
"BLACK_HOLE_BIG",
|
||||||
|
"BLACK_HOLE_GIGA",
|
||||||
|
"THUNDER_BLAST",
|
||||||
|
"FIRE_BLAST",
|
||||||
|
"EXPLOSION_LIGHT",
|
||||||
|
"POISON_BLAST",
|
||||||
|
"EXPLOSION",
|
||||||
|
"ALCOHOL_BLAST",
|
||||||
|
"WHITE_HOLE_GIGA",
|
||||||
|
"FRIEND_FLY",
|
||||||
|
"VACUUM_POWDER",
|
||||||
|
"VACUUM_LIQUID",
|
||||||
|
"VACUUM_ENTITIES",
|
||||||
|
"CLUSTERMOD",
|
||||||
|
"MANA_REDUCE",
|
||||||
|
"ARC_POISON",
|
||||||
|
"ARC_FIRE",
|
||||||
|
"ARC_GUNPOWDER",
|
||||||
|
"ARC_ELECTRIC",
|
||||||
|
"ROCKET_DOWNWARDS",
|
||||||
|
"ROCKET_OCTAGON",
|
||||||
|
"BOUNCE",
|
||||||
|
"BOUNCE_SPARK",
|
||||||
|
"BOUNCE_LASER",
|
||||||
|
"REMOVE_BOUNCE",
|
||||||
|
"BOUNCE_SMALL_EXPLOSION",
|
||||||
|
"BOUNCE_HOLE",
|
||||||
|
"BOUNCE_EXPLOSION",
|
||||||
|
"BOUNCE_LARPA",
|
||||||
|
"BOUNCE_LIGHTNING",
|
||||||
|
"BOUNCE_LASER_EMITTER",
|
||||||
|
"EXPLOSION_TINY",
|
||||||
|
"HITFX_CRITICAL_BLOOD",
|
||||||
|
"HITFX_CRITICAL_OIL",
|
||||||
|
"HITFX_CRITICAL_WATER",
|
||||||
|
"HITFX_BURNING_CRITICAL_HIT",
|
||||||
|
"CRITICAL_HIT",
|
||||||
|
"AREA_DAMAGE",
|
||||||
|
"BLOODLUST",
|
||||||
|
"DAMAGE",
|
||||||
|
"DAMAGE_RANDOM",
|
||||||
|
"DAMAGE_FOREVER",
|
||||||
|
"ZERO_DAMAGE",
|
||||||
|
"HEAVY_SHOT",
|
||||||
|
"LIGHT_SHOT",
|
||||||
|
"CRUMBLING_EARTH_PROJECTILE",
|
||||||
|
"FREEZE",
|
||||||
|
"ELECTRIC_CHARGE",
|
||||||
|
"HITFX_EXPLOSION_ALCOHOL",
|
||||||
|
"HITFX_EXPLOSION_ALCOHOL_GIGA",
|
||||||
|
"HITFX_EXPLOSION_SLIME",
|
||||||
|
"HITFX_EXPLOSION_SLIME_GIGA",
|
||||||
|
"HITFX_TOXIC_CHARM",
|
||||||
|
"COLOUR_RED",
|
||||||
|
"COLOUR_ORANGE",
|
||||||
|
"COLOUR_YELLOW",
|
||||||
|
"COLOUR_GREEN",
|
||||||
|
"COLOUR_BLUE",
|
||||||
|
"COLOUR_PURPLE",
|
||||||
|
"COLOUR_RAINBOW",
|
||||||
|
"COLOUR_INVIS",
|
||||||
|
"HEAVY_SPREAD",
|
||||||
|
"KNOCKBACK",
|
||||||
|
"LARPA_CHAOS_2",
|
||||||
|
"LARPA_CHAOS",
|
||||||
|
"LARPA_DOWNWARDS",
|
||||||
|
"LARPA_DEATH",
|
||||||
|
"LARPA_UPWARDS",
|
||||||
|
"LIFETIME_DOWN",
|
||||||
|
"LIFETIME",
|
||||||
|
"CHAIN_SHOT",
|
||||||
|
"NOLLA",
|
||||||
|
"LIGHT",
|
||||||
|
"NECROMANCY",
|
||||||
|
"ORBIT_DISCS",
|
||||||
|
"ORBIT_NUKES",
|
||||||
|
"ORBIT_FIREBALLS",
|
||||||
|
"ORBIT_LARPA",
|
||||||
|
"ORBIT_LASERS",
|
||||||
|
"LINE_ARC",
|
||||||
|
"HORIZONTAL_ARC",
|
||||||
|
"GRAVITY",
|
||||||
|
"GRAVITY_ANTI",
|
||||||
|
"FLY_UPWARDS",
|
||||||
|
"FLY_DOWNWARDS",
|
||||||
|
"ORBIT_SHOT",
|
||||||
|
"TRUE_ORBIT",
|
||||||
|
"SPIRALING_SHOT",
|
||||||
|
"PINGPONG_PATH",
|
||||||
|
"PHASING_ARC",
|
||||||
|
"CHAOTIC_ARC",
|
||||||
|
"SINEWAVE",
|
||||||
|
"HOMING_WAND",
|
||||||
|
"HOMING_CURSOR",
|
||||||
|
"HOMING_SHORT",
|
||||||
|
"AUTOAIM",
|
||||||
|
"HOMING",
|
||||||
|
"HOMING_ROTATE",
|
||||||
|
"HOMING_SHOOTER",
|
||||||
|
"ANTI_HOMING",
|
||||||
|
"HOMING_ACCELERATING",
|
||||||
|
"HOMING_AREA",
|
||||||
|
"FIREBALL_RAY_ENEMY",
|
||||||
|
"GRAVITY_FIELD_ENEMY",
|
||||||
|
"TENTACLE_RAY_ENEMY",
|
||||||
|
"LIGHTNING_RAY_ENEMY",
|
||||||
|
"HITFX_PETRIFY",
|
||||||
|
"PIERCING_SHOT",
|
||||||
|
"LASER_EMITTER_WIDER",
|
||||||
|
"QUANTUM_SPLIT",
|
||||||
|
"RANDOM_EXPLOSION",
|
||||||
|
"RANDOM_MODIFIER",
|
||||||
|
"RECOIL",
|
||||||
|
"RECOIL_DAMPER",
|
||||||
|
"RECHARGE",
|
||||||
|
"SPREAD_REDUCE",
|
||||||
|
"EXPLOSION_REMOVE",
|
||||||
|
"SLOW_BUT_STEADY",
|
||||||
|
"ENERGY_SHIELD_SHOT",
|
||||||
|
"SPEED",
|
||||||
|
"DECELERATING_SHOT",
|
||||||
|
"ACCELERATING_SHOT",
|
||||||
|
"FIZZLE",
|
||||||
|
"FLOATING_ARC",
|
||||||
|
"AVOIDING_ARC",
|
||||||
|
"CLIPPING_SHOT",
|
||||||
|
"UNSTABLE_GUNPOWDER",
|
||||||
|
"MATTER_EATER",
|
||||||
|
"EXPLOSIVE_PROJECTILE",
|
||||||
|
"FIREBALL_RAY_LINE",
|
||||||
|
"FIREBALL_RAY",
|
||||||
|
"LIGHTNING_RAY",
|
||||||
|
"TENTACLE_RAY",
|
||||||
|
"LASER_EMITTER_RAY",
|
||||||
|
"SPELLS_TO_POWER",
|
||||||
|
"ESSENCE_TO_POWER",
|
||||||
|
"ACID_TRAIL",
|
||||||
|
"FIRE_TRAIL",
|
||||||
|
"GUNPOWDER_TRAIL",
|
||||||
|
"OIL_TRAIL",
|
||||||
|
"POISON_TRAIL",
|
||||||
|
"RAINBOW_TRAIL",
|
||||||
|
"WATER_TRAIL",
|
||||||
|
"BURN_TRAIL",
|
||||||
|
"WATER_TO_POISON",
|
||||||
|
"BLOOD_TO_ACID",
|
||||||
|
"LAVA_TO_BLOOD",
|
||||||
|
"LIQUID_TO_EXPLOSION",
|
||||||
|
"TOXIC_TO_ACID",
|
||||||
|
"STATIC_TO_SAND",
|
||||||
|
"TRANSMUTATION",
|
||||||
|
"CURSE_WITHER_ELECTRICITY",
|
||||||
|
"CURSE_WITHER_EXPLOSION",
|
||||||
|
"CURSE_WITHER_MELEE",
|
||||||
|
"CURSE_WITHER_PROJECTILE",
|
||||||
|
"CURSE",
|
||||||
|
"BURST_2",
|
||||||
|
"BURST_3",
|
||||||
|
"BURST_4",
|
||||||
|
"BURST_8",
|
||||||
|
"BURST_X",
|
||||||
|
"SCATTER_2",
|
||||||
|
"SCATTER_3",
|
||||||
|
"SCATTER_4",
|
||||||
|
"I_SHAPE",
|
||||||
|
"T_SHAPE",
|
||||||
|
"PENTAGRAM_SHAPE",
|
||||||
|
"CIRCLE_SHAPE",
|
||||||
|
"Y_SHAPE",
|
||||||
|
"W_SHAPE",
|
||||||
|
"TOUCH_PISS",
|
||||||
|
"TOUCH_GRASS",
|
||||||
|
"SOILBALL",
|
||||||
|
"CIRCLE_FIRE",
|
||||||
|
"CIRCLE_ACID",
|
||||||
|
"CIRCLE_OIL",
|
||||||
|
"CIRCLE_WATER",
|
||||||
|
"MATERIAL_BLOOD",
|
||||||
|
"MATERIAL_CEMENT",
|
||||||
|
"MATERIAL_OIL",
|
||||||
|
"MATERIAL_ACID",
|
||||||
|
"MATERIAL_WATER",
|
||||||
|
"SEA_LAVA",
|
||||||
|
"SEA_ALCOHOL",
|
||||||
|
"SEA_OIL",
|
||||||
|
"SEA_WATER",
|
||||||
|
"SEA_ACID",
|
||||||
|
"SEA_ACID_GAS",
|
||||||
|
"SEA_SWAMP",
|
||||||
|
"SEA_MIMIC",
|
||||||
|
"TOUCH_BLOOD",
|
||||||
|
"TOUCH_GOLD",
|
||||||
|
"TOUCH_OIL",
|
||||||
|
"TOUCH_SMOKE",
|
||||||
|
"TOUCH_ALCOHOL",
|
||||||
|
"TOUCH_WATER",
|
||||||
|
"X_RAY",
|
||||||
|
"BLOOD_MAGIC",
|
||||||
|
"CASTER_CAST",
|
||||||
|
"I_SHOT",
|
||||||
|
"Y_SHOT",
|
||||||
|
"T_SHOT",
|
||||||
|
"W_SHOT",
|
||||||
|
"QUAD_SHOT",
|
||||||
|
"PENTA_SHOT",
|
||||||
|
"HEXA_SHOT",
|
||||||
|
"SUPER_TELEPORT_CAST",
|
||||||
|
"TELEPORT_CAST",
|
||||||
|
"LONG_DISTANCE_CAST",
|
||||||
|
"ALL_ACID",
|
||||||
|
"ALL_BLACKHOLES",
|
||||||
|
"ALL_DEATHCROSSES",
|
||||||
|
"ALL_ROCKETS",
|
||||||
|
"ALL_NUKES",
|
||||||
|
"ALL_DISCS",
|
||||||
|
"SUMMON_WANDGHOST",
|
||||||
|
"TEMPORARY_PLATFORM",
|
||||||
|
"TEMPORARY_WALL",
|
||||||
|
"MONEY_MAGIC",
|
||||||
|
"BLOOD_TO_POWER",
|
||||||
|
"RESET",
|
||||||
|
"ENERGY_SHIELD",
|
||||||
|
"ENERGY_SHIELD_SECTOR",
|
||||||
|
"TINY_GHOST",
|
||||||
|
"TORCH",
|
||||||
|
"TORCH_ELECTRIC",
|
||||||
|
"ADD_TRIGGER",
|
||||||
|
"ADD_TIMER",
|
||||||
|
"ADD_DEATH_TRIGGER",
|
||||||
|
"CESSATION",
|
||||||
|
"DIVIDE_2",
|
||||||
|
"DIVIDE_3",
|
||||||
|
"DIVIDE_4",
|
||||||
|
"DIVIDE_10",
|
||||||
|
"OMEGA",
|
||||||
|
"ZETA",
|
||||||
|
"TAU",
|
||||||
|
"SIGMA",
|
||||||
|
"PHI",
|
||||||
|
"MU",
|
||||||
|
"ALPHA",
|
||||||
|
"GAMMA",
|
||||||
|
"KANTELE_A",
|
||||||
|
"KANTELE_D",
|
||||||
|
"KANTELE_DIS",
|
||||||
|
"KANTELE_E",
|
||||||
|
"KANTELE_G",
|
||||||
|
"OCARINA_A",
|
||||||
|
"OCARINA_B",
|
||||||
|
"OCARINA_C",
|
||||||
|
"OCARINA_D",
|
||||||
|
"OCARINA_E",
|
||||||
|
"OCARINA_F",
|
||||||
|
"OCARINA_GSHARP",
|
||||||
|
"OCARINA_A2",
|
||||||
|
"RANDOM_SPELL",
|
||||||
|
"DRAW_RANDOM",
|
||||||
|
"DRAW_RANDOM_X3",
|
||||||
|
"DRAW_3_RANDOM",
|
||||||
|
"IF_PROJECTILE",
|
||||||
|
"IF_HP",
|
||||||
|
"IF_ENEMY",
|
||||||
|
"IF_HALF",
|
||||||
|
"IF_ELSE",
|
||||||
|
"IF_END",
|
||||||
|
"DUPLICATE",
|
||||||
|
"SUMMON_PORTAL",
|
||||||
|
"ALL_SPELLS",
|
||||||
|
]
|
||||||
|
|
||||||
|
ALL_PERKS = [
|
||||||
|
"CRITICAL_HIT",
|
||||||
|
"BREATH_UNDERWATER",
|
||||||
|
"EXTRA_MONEY",
|
||||||
|
"EXTRA_MONEY_TRICK_KILL",
|
||||||
|
"GOLD_IS_FOREVER",
|
||||||
|
"TRICK_BLOOD_MONEY",
|
||||||
|
"EXPLODING_GOLD",
|
||||||
|
"HOVER_BOOST",
|
||||||
|
"FASTER_LEVITATION",
|
||||||
|
"MOVEMENT_FASTER",
|
||||||
|
"LOW_GRAVITY",
|
||||||
|
"HIGH_GRAVITY",
|
||||||
|
"SPEED_DIVER",
|
||||||
|
"STRONG_KICK",
|
||||||
|
"TELEKINESIS",
|
||||||
|
"REPELLING_CAPE",
|
||||||
|
"EXPLODING_CORPSES",
|
||||||
|
"SAVING_GRACE",
|
||||||
|
"INVISIBILITY",
|
||||||
|
"GLOBAL_GORE",
|
||||||
|
"REMOVE_FOG_OF_WAR",
|
||||||
|
"LEVITATION_TRAIL",
|
||||||
|
"VAMPIRISM",
|
||||||
|
"EXTRA_HP",
|
||||||
|
"HEARTS_MORE_EXTRA_HP",
|
||||||
|
"GLASS_CANNON",
|
||||||
|
"LOW_HP_DAMAGE_BOOST",
|
||||||
|
"RESPAWN",
|
||||||
|
"WORM_ATTRACTOR",
|
||||||
|
"WORM_DETRACTOR",
|
||||||
|
"RADAR_ENEMY",
|
||||||
|
"FOOD_CLOCK",
|
||||||
|
"WAND_RADAR",
|
||||||
|
"ITEM_RADAR",
|
||||||
|
"MOON_RADAR",
|
||||||
|
"PROTECTION_FIRE",
|
||||||
|
"PROTECTION_RADIOACTIVITY",
|
||||||
|
"PROTECTION_EXPLOSION",
|
||||||
|
"PROTECTION_MELEE",
|
||||||
|
"PROTECTION_ELECTRICITY",
|
||||||
|
"TELEPORTITIS",
|
||||||
|
"TELEPORTITIS_DODGE",
|
||||||
|
"STAINLESS_ARMOUR",
|
||||||
|
"EDIT_WANDS_EVERYWHERE",
|
||||||
|
"NO_WAND_EDITING",
|
||||||
|
"WAND_EXPERIMENTER",
|
||||||
|
"ADVENTURER",
|
||||||
|
"ABILITY_ACTIONS_MATERIALIZED",
|
||||||
|
"PROJECTILE_HOMING",
|
||||||
|
"PROJECTILE_HOMING_SHOOTER",
|
||||||
|
"UNLIMITED_SPELLS",
|
||||||
|
"FREEZE_FIELD",
|
||||||
|
"FIRE_GAS",
|
||||||
|
"DISSOLVE_POWDERS",
|
||||||
|
"BLEED_SLIME",
|
||||||
|
"BLEED_OIL",
|
||||||
|
"BLEED_GAS",
|
||||||
|
"SHIELD",
|
||||||
|
"REVENGE_EXPLOSION",
|
||||||
|
"REVENGE_TENTACLE",
|
||||||
|
"REVENGE_RATS",
|
||||||
|
"REVENGE_BULLET",
|
||||||
|
"ATTACK_FOOT",
|
||||||
|
"LEGGY_FEET",
|
||||||
|
"PLAGUE_RATS",
|
||||||
|
"VOMIT_RATS",
|
||||||
|
"CORDYCEPS",
|
||||||
|
"MOLD",
|
||||||
|
"WORM_SMALLER_HOLES",
|
||||||
|
"PROJECTILE_REPULSION",
|
||||||
|
"RISKY_CRITICAL",
|
||||||
|
"FUNGAL_DISEASE",
|
||||||
|
"PROJECTILE_SLOW_FIELD",
|
||||||
|
"PROJECTILE_REPULSION_SECTOR",
|
||||||
|
"PROJECTILE_EATER_SECTOR",
|
||||||
|
"ORBIT",
|
||||||
|
"ANGRY_GHOST",
|
||||||
|
"HUNGRY_GHOST",
|
||||||
|
"DEATH_GHOST",
|
||||||
|
"HOMUNCULUS",
|
||||||
|
"ELECTRICITY",
|
||||||
|
"ATTRACT_ITEMS",
|
||||||
|
"EXTRA_KNOCKBACK",
|
||||||
|
"LOWER_SPREAD",
|
||||||
|
"LOW_RECOIL",
|
||||||
|
"BOUNCE",
|
||||||
|
"FAST_PROJECTILES",
|
||||||
|
"ALWAYS_CAST",
|
||||||
|
"EXTRA_MANA",
|
||||||
|
"NO_MORE_SHUFFLE",
|
||||||
|
"NO_MORE_KNOCKBACK",
|
||||||
|
"DUPLICATE_PROJECTILE",
|
||||||
|
"FASTER_WANDS",
|
||||||
|
"EXTRA_SLOTS",
|
||||||
|
"CONTACT_DAMAGE",
|
||||||
|
"EXTRA_PERK",
|
||||||
|
"PERKS_LOTTERY",
|
||||||
|
"GAMBLE",
|
||||||
|
"EXTRA_SHOP_ITEM",
|
||||||
|
"GENOME_MORE_HATRED",
|
||||||
|
"GENOME_MORE_LOVE",
|
||||||
|
"PEACE_WITH_GODS",
|
||||||
|
"MANA_FROM_KILLS",
|
||||||
|
"ANGRY_LEVITATION",
|
||||||
|
"LASER_AIM",
|
||||||
|
"PERSONAL_LASER",
|
||||||
|
"MEGA_BEAM_STONE",
|
||||||
|
"IRON_STOMACH",
|
||||||
|
]
|
||||||
@ -6,12 +6,14 @@ from noita.api import router as noita_router
|
|||||||
|
|
||||||
# Create the main API instance
|
# Create the main API instance
|
||||||
api = NinjaAPI(
|
api = NinjaAPI(
|
||||||
title="Opus Magnum Submission API",
|
title="PolyLAN Submission API",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
description="""API for managing Opus Magnum puzzle submissions.
|
description="""API for managing Opus Magnum puzzle submissions, and Noita runs.
|
||||||
|
|
||||||
The Opus Magnum Submission API allows clients to upload, manage, validate, and review puzzle solution submissions for the Opus Magnum puzzle game community.
|
The Opus Magnum Submission API allows clients to upload, manage, validate, and review puzzle solution submissions for the Opus Magnum puzzle game community.
|
||||||
It provides features for user authentication, puzzle listing, submission uploads, automated and manual OCR validation, and administrative workflows.
|
It provides features for user authentication, puzzle listing, submission uploads, automated and manual OCR validation, and administrative workflows.
|
||||||
|
|
||||||
|
The Noita Submission API allows clients to upload the result of the log file of the PolyLAN noita mod. It parses the output, and store each objectiv made by the user.
|
||||||
""",
|
""",
|
||||||
openapi_extra={
|
openapi_extra={
|
||||||
"info": {
|
"info": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
|
interface Objective {
|
||||||
|
objectiv_id: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
const userInfo = ref({
|
const userInfo = ref({
|
||||||
username: "Player",
|
username: "Player",
|
||||||
@ -11,6 +16,8 @@ const userInfo = ref({
|
|||||||
const uploadedFiles = ref<File[]>([]);
|
const uploadedFiles = ref<File[]>([]);
|
||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
const isDragover = ref(false);
|
const isDragover = ref(false);
|
||||||
|
const objectives = ref<Objective[]>([]);
|
||||||
|
const isLoadingObjectives = ref(false);
|
||||||
|
|
||||||
const handleFileUpload = (event: Event) => {
|
const handleFileUpload = (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@ -78,6 +85,24 @@ const submitRun = async () => {
|
|||||||
const goHome = () => {
|
const goHome = () => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchObjectives = async () => {
|
||||||
|
isLoadingObjectives.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/noita/objectives");
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch objectives");
|
||||||
|
|
||||||
|
objectives.value = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching objectives:", error);
|
||||||
|
} finally {
|
||||||
|
isLoadingObjectives.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchObjectives();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -149,25 +174,15 @@ const goHome = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Upload Area -->
|
<!-- Upload Area -->
|
||||||
<div
|
<div @dragover="handleDragover" @dragleave="handleDragleave" @drop="handleDrop" :class="[
|
||||||
@dragover="handleDragover"
|
'border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer bg-base-200/50 mb-6',
|
||||||
@dragleave="handleDragleave"
|
isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary'
|
||||||
@drop="handleDrop"
|
]">
|
||||||
:class="[
|
<input type="file" multiple @change="handleFileUpload" class="hidden" id="file-upload"
|
||||||
'border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer bg-base-200/50 mb-6',
|
accept="video/*,image/*" />
|
||||||
isDragover ? 'border-primary bg-primary/10' : 'border-base-300 hover:border-primary'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
@change="handleFileUpload"
|
|
||||||
class="hidden"
|
|
||||||
id="file-upload"
|
|
||||||
accept="video/*,image/*"
|
|
||||||
/>
|
|
||||||
<label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3">
|
<label for="file-upload" class="cursor-pointer flex flex-col items-center gap-3">
|
||||||
<i :class="['mdi text-4xl', isDragover ? 'mdi-cloud-check text-primary' : 'mdi-file-upload text-base-content/50']"></i>
|
<i
|
||||||
|
:class="['mdi text-4xl', isDragover ? 'mdi-cloud-check text-primary' : 'mdi-file-upload text-base-content/50']"></i>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">Click to upload or drag and drop</p>
|
<p class="font-semibold">Click to upload or drag and drop</p>
|
||||||
<p class="text-sm text-base-content/70">Video or image files (MP4, PNG, etc.)</p>
|
<p class="text-sm text-base-content/70">Video or image files (MP4, PNG, etc.)</p>
|
||||||
@ -179,16 +194,14 @@ const goHome = () => {
|
|||||||
<div v-if="uploadedFiles.length > 0" class="mb-6">
|
<div v-if="uploadedFiles.length > 0" class="mb-6">
|
||||||
<p class="font-semibold mb-3">Selected Files:</p>
|
<p class="font-semibold mb-3">Selected Files:</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="(file, index) in uploadedFiles" :key="index" class="flex items-center gap-3 bg-base-200 p-3 rounded-lg">
|
<div v-for="(file, index) in uploadedFiles" :key="index"
|
||||||
|
class="flex items-center gap-3 bg-base-200 p-3 rounded-lg">
|
||||||
<i class="mdi mdi-file text-primary"></i>
|
<i class="mdi mdi-file text-primary"></i>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium truncate">{{ file.name }}</p>
|
<p class="font-medium truncate">{{ file.name }}</p>
|
||||||
<p class="text-xs text-base-content/70">{{ (file.size / 1024 / 1024).toFixed(2) }} MB</p>
|
<p class="text-xs text-base-content/70">{{ (file.size / 1024 / 1024).toFixed(2) }} MB</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button @click="uploadedFiles.splice(index, 1)" class="btn btn-ghost btn-xs">
|
||||||
@click="uploadedFiles.splice(index, 1)"
|
|
||||||
class="btn btn-ghost btn-xs"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -201,11 +214,8 @@ const goHome = () => {
|
|||||||
<i class="mdi mdi-folder-open mr-2"></i>
|
<i class="mdi mdi-folder-open mr-2"></i>
|
||||||
Choose Files
|
Choose Files
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button @click="submitRun" :disabled="uploadedFiles.length === 0 || isUploading"
|
||||||
@click="submitRun"
|
:class="['btn btn-primary flex-1', { 'loading': isUploading }]">
|
||||||
:disabled="uploadedFiles.length === 0 || isUploading"
|
|
||||||
:class="['btn btn-primary flex-1', { 'loading': isUploading }]"
|
|
||||||
>
|
|
||||||
<i v-if="!isUploading" class="mdi mdi-send mr-2"></i>
|
<i v-if="!isUploading" class="mdi mdi-send mr-2"></i>
|
||||||
{{ isUploading ? 'Submitting...' : 'Submit Run' }}
|
{{ isUploading ? 'Submitting...' : 'Submit Run' }}
|
||||||
</button>
|
</button>
|
||||||
@ -216,6 +226,44 @@ const goHome = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Objectives Table -->
|
||||||
|
<div class="card bg-base-100 shadow-lg mt-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-6">
|
||||||
|
<i class="mdi mdi-view-list text-purple-500 mr-2"></i>
|
||||||
|
Your Objectives
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-if="isLoadingObjectives" class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="objectives.length === 0" class="text-center py-8">
|
||||||
|
<p class="text-base-content/70 mb-2">No objectives completed yet</p>
|
||||||
|
<p class="text-sm text-base-content/50">Submit your runs to unlock objectives!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Objective ID</th>
|
||||||
|
<th class="text-right">Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="obj in objectives" :key="obj.objectiv_id">
|
||||||
|
<td class="font-medium">{{ obj.objectiv_id }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<span class="badge badge-primary badge-lg">{{ obj.count }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user