diff --git a/polylan_submitter/noita/admin.py b/polylan_submitter/noita/admin.py index 846f6b4..e1d7f89 100644 --- a/polylan_submitter/noita/admin.py +++ b/polylan_submitter/noita/admin.py @@ -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")}), + ) diff --git a/polylan_submitter/noita/api.py b/polylan_submitter/noita/api.py index 3bce783..14087fd 100644 --- a/polylan_submitter/noita/api.py +++ b/polylan_submitter/noita/api.py @@ -3,13 +3,20 @@ from django.core.files.base import ContentFile from ninja import Router, File from ninja.files import UploadedFile -from .models import LogfileSubmission +from noita.schemas import ObjectivOut + +from .models import LogfileSubmission, Objectiv from .schemas import NoitaSubmissionOut 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) def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)): """ diff --git a/polylan_submitter/noita/migrations/0003_objectiv.py b/polylan_submitter/noita/migrations/0003_objectiv.py new file mode 100644 index 0000000..b717c9c --- /dev/null +++ b/polylan_submitter/noita/migrations/0003_objectiv.py @@ -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, + ), + ), + ], + ), + ] diff --git a/polylan_submitter/noita/migrations/0004_objectiv_count.py b/polylan_submitter/noita/migrations/0004_objectiv_count.py new file mode 100644 index 0000000..a937dd2 --- /dev/null +++ b/polylan_submitter/noita/migrations/0004_objectiv_count.py @@ -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), + ), + ] diff --git a/polylan_submitter/noita/migrations/0005_remove_objectiv_updated_at.py b/polylan_submitter/noita/migrations/0005_remove_objectiv_updated_at.py new file mode 100644 index 0000000..fd1eacb --- /dev/null +++ b/polylan_submitter/noita/migrations/0005_remove_objectiv_updated_at.py @@ -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", + ), + ] diff --git a/polylan_submitter/noita/migrations/0006_objectivpoint.py b/polylan_submitter/noita/migrations/0006_objectivpoint.py new file mode 100644 index 0000000..dbd2813 --- /dev/null +++ b/polylan_submitter/noita/migrations/0006_objectivpoint.py @@ -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)), + ], + ), + ] diff --git a/polylan_submitter/noita/models.py b/polylan_submitter/noita/models.py index d320c9a..c7210f6 100644 --- a/polylan_submitter/noita/models.py +++ b/polylan_submitter/noita/models.py @@ -42,3 +42,18 @@ class LogfileSubmission(models.Model): updated_at = models.DateTimeField(auto_now=True) 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) diff --git a/polylan_submitter/noita/schemas.py b/polylan_submitter/noita/schemas.py index e456612..f5a274f 100644 --- a/polylan_submitter/noita/schemas.py +++ b/polylan_submitter/noita/schemas.py @@ -1,9 +1,17 @@ from typing import Optional -from pydantic import BaseModel 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 user_id: Optional[int] username: Optional[str] @@ -11,6 +19,3 @@ class NoitaSubmissionOut(BaseModel): content_type: str created_at: datetime processed: bool - - class Config: - from_attributes = True diff --git a/polylan_submitter/noita/services/__init__.py b/polylan_submitter/noita/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polylan_submitter/noita/services/decode.py b/polylan_submitter/noita/services/decode.py new file mode 100644 index 0000000..4271f68 --- /dev/null +++ b/polylan_submitter/noita/services/decode.py @@ -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.") diff --git a/polylan_submitter/noita/services/objectives.py b/polylan_submitter/noita/services/objectives.py new file mode 100644 index 0000000..a7dd53d --- /dev/null +++ b/polylan_submitter/noita/services/objectives.py @@ -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"]) diff --git a/polylan_submitter/noita/services/spells.py b/polylan_submitter/noita/services/spells.py new file mode 100644 index 0000000..46b8493 --- /dev/null +++ b/polylan_submitter/noita/services/spells.py @@ -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", +] diff --git a/polylan_submitter/polylan_submitter/api.py b/polylan_submitter/polylan_submitter/api.py index b28b031..8957e4e 100644 --- a/polylan_submitter/polylan_submitter/api.py +++ b/polylan_submitter/polylan_submitter/api.py @@ -6,12 +6,14 @@ from noita.api import router as noita_router # Create the main API instance api = NinjaAPI( - title="Opus Magnum Submission API", + title="PolyLAN Submission API", 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. 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={ "info": { diff --git a/polylan_submitter/src/Noita.vue b/polylan_submitter/src/Noita.vue index 5f0176c..4f498c5 100644 --- a/polylan_submitter/src/Noita.vue +++ b/polylan_submitter/src/Noita.vue @@ -1,5 +1,10 @@