From 9e6b336a6f5943c6336705256304d00be8a549fb Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:20:04 +0200 Subject: [PATCH 01/10] =?UTF-8?q?feat(tools):=20=E2=9C=A8=20Filet=20de=20s?= =?UTF-8?q?=C3=A9curit=C3=A9:=20d=C3=A9rive=20de=20build/=20et=20d=C3=A9te?= =?UTF-8?q?ction=20des=20fusions=20de=20fonctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyrightconfig.json | 2 +- tools/check_output_drift.py | 90 +++++++++++++++++++++ tools/report_merged_functions.py | 132 +++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 tools/check_output_drift.py create mode 100644 tools/report_merged_functions.py diff --git a/pyrightconfig.json b/pyrightconfig.json index eda844e61..e9d4570f1 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1 +1 @@ -{ "typeCheckingMode": "strict", "include": ["src"], "exclude": ["**/__pycache__"] } +{ "typeCheckingMode": "strict", "include": ["src", "tools"], "exclude": ["**/__pycache__"] } diff --git a/tools/check_output_drift.py b/tools/check_output_drift.py new file mode 100644 index 000000000..6efe50d2b --- /dev/null +++ b/tools/check_output_drift.py @@ -0,0 +1,90 @@ +""" Golden-file guard: rebuild the project and fail if the tracked build/ output moved. + +Any purely structural refactoring must leave the generated output byte for byte identical. +Run this after moving code around: a non-empty diff means the refactor changed behaviour. +""" +# Imports +import subprocess +import sys +from pathlib import Path + +import stouputils as stp + +# Constants +ROOT: Path = Path(__file__).resolve().parent.parent +""" Repository root, the working directory every command below runs from. """ + +OUTPUT_FOLDER: str = "build" +""" Tracked folder holding the generated datapack and resource pack. """ + +MAX_LISTED_FILES: int = 25 +""" How many drifted paths to print before summarizing the rest. """ + + +# Classes +class OutputDrift: + + @staticmethod + def run(*args: str) -> subprocess.CompletedProcess[str]: + """ Run a command from the repository root and capture its output. + + Args: + *args (str): Command and its arguments, ex: "git", "status" + Returns: + subprocess.CompletedProcess[str]: The completed process, never raising on a non-zero code + """ + return subprocess.run(args, cwd=ROOT, capture_output=True, text=True, check=False) + + @staticmethod + def drifted_paths() -> list[str]: + """ List every tracked or untracked path under the output folder that differs from HEAD. + + Returns: + list[str]: Porcelain paths, ex: ["build/datapack/data/switch/function/tick.mcfunction"] + """ + status: subprocess.CompletedProcess[str] = OutputDrift.run("git", "status", "--porcelain", "--", OUTPUT_FOLDER) + return [line[3:].strip().strip('"') for line in status.stdout.splitlines() if line.strip()] + + @staticmethod + def build() -> bool: + """ Rebuild the project with the StewBeet CLI. + + Returns: + bool: Whether the build succeeded + """ + build: subprocess.CompletedProcess[str] = OutputDrift.run("stewbeet") + if build.returncode != 0: + stp.error(f"Build failed with exit code {build.returncode}:\n{build.stdout}\n{build.stderr}") + return build.returncode == 0 + + @staticmethod + def main() -> int: + """ Rebuild, then compare the output folder against HEAD. + + Returns: + int: Process exit code, 0 when the output is unchanged + """ + # A dirty output folder before building makes the comparison meaningless + if OutputDrift.drifted_paths(): + stp.error(f"'{OUTPUT_FOLDER}/' already differs from HEAD before building, commit or restore it first") + return 1 + + if not OutputDrift.build(): + return 1 + + drifted: list[str] = OutputDrift.drifted_paths() + if not drifted: + stp.info(f"No drift: '{OUTPUT_FOLDER}/' is identical to HEAD") + return 0 + + stp.error(f"{len(drifted)} file(s) drifted in '{OUTPUT_FOLDER}/':") + for path in drifted[:MAX_LISTED_FILES]: + stp.error(f" {path}") + if len(drifted) > MAX_LISTED_FILES: + stp.error(f" ... and {len(drifted) - MAX_LISTED_FILES} more") + return 1 + + +if __name__ == "__main__": + sys.exit(OutputDrift.main()) + diff --git a/tools/report_merged_functions.py b/tools/report_merged_functions.py new file mode 100644 index 000000000..5386b5e26 --- /dev/null +++ b/tools/report_merged_functions.py @@ -0,0 +1,132 @@ +""" Report every datapack function written by more than one emitter during the build. + +write_function() appends by default, so two emitters targeting the same path merge silently in +call order. That is a legitimate builder pattern in a few places and a landmine everywhere else. +This runs the pipeline in a throwaway output folder and fails on any merge not declared below. +""" +# Imports +import shutil +import sys +import tempfile +from collections import Counter +from fnmatch import fnmatch +from pathlib import Path +from typing import Any, ClassVar + +import stewbeet +import stouputils as stp +from beet import Function, run_beet +from stewbeet.core.utils.io import functions as functions_module # pyright: ignore[reportMissingTypeStubs] +from stewbeet.utils import get_project_config + +# Constants +DECLARED_MERGES: tuple[str, ...] = ( + "switch:shop/*", + "switch:player/username_change/update_shops", + "switch:engine/pop_ups/schedule", + "switch:modes/*/give_items", + "switch:modes/sheepwars/team_and_give", + "switch:modes/build_battle/preparation/themes_list", + "switch:load", + "switch:v*/*", +) +""" Function paths intentionally built by several successive appends, as fnmatch patterns. + +The shop family accumulates one block per upgrade, pop_ups/schedule one revoke per mode and +language, and the give_items family is a hand-written body completed by the Kit renderer. +The switch:load and switch:v*/* entries belong to StewBeet's versioned loading scheme. +""" + +SKIPPED_PLUGINS: tuple[str, ...] = ("archive", "copy_to_destination", "compute_sha1", "merge_smithed_weld") +""" Pipeline steps dropped for the report run: they only package and ship the output. """ + + +# Classes +class MergedFunctions: + + WRITES: ClassVar[Counter[str]] = Counter() + """ How many times each function path was written during the build. """ + + original = staticmethod(functions_module.write_function) + """ The unpatched StewBeet write_function, kept so the patch can delegate to it. """ + + @staticmethod + def patched_write_function(path: str, content: str, *args: Any, **kwargs: Any) -> Function | None: + """ Count the write, then delegate to the real implementation. + + Args: + path (str): The function path, ex: "switch:modes/tnt_run/tick" + content (str): The function body + Returns: + Function | None: Whatever StewBeet returned + """ + MergedFunctions.WRITES[path.removesuffix(".mcfunction")] += 1 + return MergedFunctions.original(path, content, *args, **kwargs) + + @staticmethod + def build_into(output: Path) -> None: + """ Run the project pipeline, writing to a throwaway output folder. + + Args: + output (Path): Folder receiving the generated packs, never the tracked build/ + """ + config = get_project_config() + config.output = str(output) + config.pipeline = [step for step in config.pipeline + if not any(skipped in str(step) for skipped in SKIPPED_PLUGINS)] + + # The project cache holds the decoded assets, without it the run re-downloads everything + with run_beet(config, cache=True): + pass + + @staticmethod + def is_declared(path: str) -> bool: + """ Whether a merged path is a known, intentional builder. + + Args: + path (str): The function path, ex: "switch:shop/sheepwars" + Returns: + bool: True when the path matches one of DECLARED_MERGES + + Examples: + >>> MergedFunctions.is_declared("switch:shop/sheepwars") + True + >>> MergedFunctions.is_declared("switch:modes/tnt_run/tick") + False + """ + return any(fnmatch(path, pattern) for pattern in DECLARED_MERGES) + + @staticmethod + def main() -> int: + """ Build, then report every function path written more than once. + + Returns: + int: Process exit code, 0 when every merge is declared + """ + functions_module.write_function = MergedFunctions.patched_write_function + stewbeet.write_function = MergedFunctions.patched_write_function + + output: Path = Path(tempfile.mkdtemp(prefix="switch_merge_report_")) + try: + MergedFunctions.build_into(output) + finally: + shutil.rmtree(output, ignore_errors=True) + + merged: dict[str, int] = {path: count for path, count in MergedFunctions.WRITES.items() if count > 1} + undeclared: dict[str, int] = {path: count for path, count in merged.items() if not MergedFunctions.is_declared(path)} + + stp.info(f"{sum(MergedFunctions.WRITES.values())} writes over {len(MergedFunctions.WRITES)} distinct paths") + stp.info(f"{len(merged)} merged path(s), {len(merged) - len(undeclared)} declared") + + if not undeclared: + return 0 + + stp.error(f"{len(undeclared)} undeclared merge(s): add merge=True at the call site, or declare the path") + for path, count in sorted(undeclared.items(), key=lambda item: -item[1]): + stp.error(f" {count}x {path}") + return 1 + + +if __name__ == "__main__": + sys.exit(MergedFunctions.main()) + From e98aa1a4cef2efa21de07f69184318faef062472 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:27:06 +0200 Subject: [PATCH 02/10] =?UTF-8?q?refactor(modes):=20=E2=99=BB=EF=B8=8F=20M?= =?UTF-8?q?ODES=20typ=C3=A9=20en=20dataclasses=20frozen=20(sortie=20identi?= =?UTF-8?q?que)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/datapack/misc_links/starting_pop_ups.py | 4 +- src/datapack/modes/_common/main.py | 1 + src/datapack/modes/capture_the_flag/kits.py | 1 + src/datapack/modes/definitions.py | 1152 +++++++++++-------- src/datapack/modes/one_shot/main.py | 1 + src/datapack/modes/rush_the_point/kits.py | 1 + src/datapack/modes/spec.py | 117 ++ src/datapack/survival_maps/utils.py | 2 +- 8 files changed, 782 insertions(+), 497 deletions(-) create mode 100644 src/datapack/modes/spec.py diff --git a/src/datapack/misc_links/starting_pop_ups.py b/src/datapack/misc_links/starting_pop_ups.py index b3347812b..ac9ae176e 100644 --- a/src/datapack/misc_links/starting_pop_ups.py +++ b/src/datapack/misc_links/starting_pop_ups.py @@ -16,8 +16,8 @@ def main() -> None: # For each minigame, for mode in MODES: - mode_id: str = mode["id"] - mode_name: str = mode["name_fr"] if lang == "fr" else mode.get(f"name_{lang}", mode["name_fr"]) + mode_id: str = mode.id + mode_name: str = mode.display_name(lang) # Prepare the title of the advancement title: list[JsonDict] = [{"text":"'","color":"aqua"},{"text":mode_name,"color":"yellow"}] diff --git a/src/datapack/modes/_common/main.py b/src/datapack/modes/_common/main.py index 61a97f3c4..67ffa1012 100644 --- a/src/datapack/modes/_common/main.py +++ b/src/datapack/modes/_common/main.py @@ -5,6 +5,7 @@ from ...kits import Kit, KitItem + def pvp_arena_kit() -> Kit: """ The melee loadout shared by castagne and pvpswap (castagne adds a fishing rod of its own). Built lazily: the axe's modifier id embeds the project namespace, unknown at import time. """ diff --git a/src/datapack/modes/capture_the_flag/kits.py b/src/datapack/modes/capture_the_flag/kits.py index eef09b248..6fed57fc2 100644 --- a/src/datapack/modes/capture_the_flag/kits.py +++ b/src/datapack/modes/capture_the_flag/kits.py @@ -9,6 +9,7 @@ they are built lazily by classes() instead of living at module level: the stewbeet CLI imports this module before Mem.ctx exists. """ +# ruff: noqa: E501 # Imports from stewbeet import Mem diff --git a/src/datapack/modes/definitions.py b/src/datapack/modes/definitions.py index 8180f058d..3a9a15a6d 100644 --- a/src/datapack/modes/definitions.py +++ b/src/datapack/modes/definitions.py @@ -1,517 +1,681 @@ +""" Declarative catalogue of every minigame and vote group, and the storage entries built from it. +MODES drives the vote message, the lore, the starting pop-ups and the stats objectives. A mode +generates even when its entry is commented out here: only its presence in the vote depends on it. """ -# French -# data modify storage switch:main minigames append value {min_players:2 ,max_players:-1 ,id:"replicate_the_build" ,name_fr:"Replicate The Build" ,lore_fr:["",{"text":"[Replicate The Build]\n","color":"yellow"}, {"text":"Refaites le build donné le plus rapidement possible, et sans vous tromper !\n"}, {"text":"\n[Estimation : 1-6 mins]","color":"gold"}, {"text":"\n[Inspiration : Mineplex]","color":"green"}, {"text":"\n[Proposé/Développé par AirDox]","color":"aqua"}]} - -# data modify storage switch:main minigames[{id:"replicate_the_build"}].lore_en set value ["", {"text":"[Replicate The Build]\n","color":"yellow"}, {"text":"Repeat the given build as quickly as possible, without making any mistakes!\n"}, {"text":"\n[Estimated: 1-6 mins]","color":"gold"}, {"text":"\n[Inspiration: Mineplex]","color":"green"}, {"text":"\n[Suggested/Developed by AirDox]","color":"aqua"}]} -""" # noqa: E501 - # Imports import json from stewbeet import Mem, TextComponent, write_function from stouputils.typing import JsonDict -# Modes list -MODES: list[JsonDict] = [ - { - "min_players":1, "max_players":-1, "id":"pitch_creep", "group":"creepers", "name_fr":"Pitch Creep", - "estimated_time": "1m30s", "inspiration": "Pitchout/Épicube", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Survivez en repoussant les creepers\n"},{"text":"dans une arène fermée.\n"}], - "en": [{"text":"Survive by pushing away creepers\n"},{"text":"in a closed arena.\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"laser_game", "name_fr":"Laser Game", - "estimated_time": "2m30s", "inspiration": "The physical world", "suggested_by": "Vigo", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Affrontez l'équipe adverse grâce à vos fusils-laser et faites\n"},{"text":"le plus d'élimination possible pendant un temps imparti !\n"}], - "en": [{"text":"Battle the enemy team with your laser rifles and get\n"},{"text":"as many takedowns as possible within the allotted time!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"warden_escape", "name_fr":"Warden Escape", - "estimated_time": "1m30s", "inspiration": "Minecraft 1.19", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Survivez aux wardens dans\n"},{"text":"une arène fermée.\n"}], - "en": [{"text":"Survive the wardens in\n"},{"text":"a closed arena.\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"pitchout", "name_fr":"Pitchout", - "estimated_time": "1-5 mins", "inspiration": "Épicube", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Poussez vos adversaires dans l'eau\n"},{"text":"et soyez le dernier survivant !\n"}], - "en": [{"text":"Push your opponents into the water\n"},{"text":"and be the last survivor!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"creeper_apocalypse", "group":"creepers", "name_fr":"Creeper Apocalypse", - "estimated_time": "1m30s", "inspiration": "Expiration", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Survivez aux creepers qui se\n"},{"text":"multiplient et explosent.\n"}], - "en": [{"text":"Survive the creepers that\n"},{"text":"duplicate and explode.\n"}] - }, - }, - { - "min_players":4, "max_players":-1, "id":"traitors_game", "name_fr":"Traitors Game", - "estimated_time": "4-10 mins", "inspiration": "Taupe Gun UHC", "suggested_by": "Luxio", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Des traitres se cachent parmi les joueurs.\n"},{"text":"Innocents contre Traitres, quel camp va-t-il gagner ?\n"}], - "en": [{"text":"Traitors are hinding among players.\n"},{"text":"Innocents versus Traitors, which side will win?\n"}] - }, - }, - # { - # "min_players":1, "max_players":-1, "id":"boat_race", "name_fr":"Boat Race", - # "estimated_time": "3-8 mins", "inspiration": "Expiration", "suggested_by": "Stoupy", "developed_by": "LTHCTheMaster", - # "description": { - # "fr": [{"text":"Battez-vous pour atteindre la ligne d'arrivée\n"},{"text":"en premier pour remporter la partie !\n"}], - # "en": [{"text":"Race for reaching the finish line\n"},{"text":"in first to claim the win!\n"}] - # }, - # }, - { - "min_players":2, "max_players":-1, "id":"spectres_game", "name_fr":"Spectres Game", - "estimated_time": "4-10 mins", "inspiration": "Expiration", "suggested_by": "Luxio", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Deux camps : un avec que des joueurs visibles,\n"},{"text":"l'autre avec des spectres, quel camp va-t-il gagner ?\n"}], - "en": [{"text":"Two sides: one with visible players,\n"},{"text":"the other with spectres, which side will win?\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"race", "name_fr":"Race", - "estimated_time": "3-6 mins", "inspiration": "Racing games", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Battez-vous pour atteindre la ligne d'arrivée\n"},{"text":"en premier pour remporter la partie !\n"}], - "en": [{"text":"Race for reaching the finish line\n"},{"text":"in first to claim the win!\n"}] - }, - }, - { - "min_players":8, "max_players":-1, "id":"rush_the_point", "group":"ctpf", "name_fr":"Rush The Point", - "estimated_time": "7-10 mins", "inspiration": "PluginLess", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Capturez les points centraux dans le vide\n"},{"text":"avec vos classes spéciales.\n"}], - "en": [{"text":"Capture the central points in the void\n"},{"text":"with your special classes.\n"}] - }, - }, - { - "min_players":8, "max_players":-1, "id":"rush_the_flag", "group":"ctpf", "name_fr":"Rush The Flag", - "estimated_time": "5-10 mins", "inspiration": "PluginLess", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Volez le drapeau ennemi\n"},{"text":"avec vos classes spéciales.\n"}], - "en": [{"text":"Steal the enemy flag\n"},{"text":"with your special classes.\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"spleef", "name_fr":"Spleef", - "estimated_time": "1-4 mins", "inspiration": "everywhere", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Creusez sous vos adversaires\n"},{"text":"pour les faire tomber !\n"}], - "en": [{"text":"Dig below your opponents\n"},{"text":"to make them fall!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"de_a_coudre", "group":"a_coudre", "name_fr":"Dé à Coudre", - "estimated_time": "3-6 mins", "inspiration": "OneCube point system", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Affrontez les autres joueurs dans un saut,\n"},{"text":"dans une piscine, iconique de Minecraft !\n"}], - "en": [{"text":"Take on the other players in\n"},{"text":"Minecraft's iconic pool jump!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"feed_fast", "name_fr":"Feed Fast", - "estimated_time": "60s", "inspiration": "Le diabète", "suggested_by": "Arobaze", "developed_by": "LTHCTheMaster", - "description": { - "fr": [{"text":"Mangez le plus de nourriture\n"},{"text":"possible pour gagner !\n"}], - "en": [{"text":"Eat the most food\n"},{"text":"possible to win!\n"}] - }, - }, - { - "min_players":1, "max_players":8, "id":"mlg_a_coudre", "group":"a_coudre", "name_fr":"MLG à Coudre", - "estimated_time": "1-6 mins", "inspiration": "Dé à Coudre", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Sautez et utilisez votre seau d'eau\n"},{"text":"pour survivre à la chute !\n"}], - "en": [{"text":"Jump and use your water bucket\n"},{"text":"to survive the fall!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"protect_the_king", "name_fr":"Protect The King", - "estimated_time": "1-4 mins", "inspiration": "Charlemagne", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Tuez le roi ennemi ou\n"},{"text":"protégez le vôtre !\n"}], - "en": [{"text":"Kill the enemy king or\n"},{"text":"protect your own!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"castagne", "name_fr":"Castagne RP", - "estimated_time": "1-5 mins", "inspiration": "Domestic Violence", "suggested_by": "JustNoob", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Battez-vous au corps à corps\n"},{"text":"et soyez le dernier survivant !\n"}], - "en": [{"text":"Fight hand-to-hand combat\n"},{"text":"and be the last survivor!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"layers_2_teams", "group":"layers", "name_fr":"Layers 2 Teams", - "estimated_time": "4-10 mins", "inspiration": "PenguinGames", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Équipez-vous rapidement et détruisez\n"},{"text":"l'équipe adverse située de l'autre côté du mur !\n"}], - "en": [{"text":"Gear up quickly and destroy the enemy team\n"},{"text":"on the other side of the wall!\n"}] - }, - }, - { - "min_players":4, "max_players":-1, "id":"layers_4_teams", "group":"layers", "name_fr":"Layers 4 Teams", - "estimated_time": "4-10 mins", "inspiration": "PenguinGames", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Course d'équipement entre\n"},{"text":"4 équipes, éliminez vos ennemis !\n"}], - "en": [{"text":"Equipment race between\n"},{"text":"4 teams, eliminate your enemies!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"thunder_spear", "name_fr":"Thunder Spear", - "estimated_time": "2m30s", "inspiration": "SNK/Attack On Titan", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Utilisez votre équipement tridimensionnel\n"},{"text":"et vos lances explosives !\n"}], - "en": [{"text":"Use your three-dimensional maneuver gear\n"},{"text":"and explosive spears!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"tnt_run", "name_fr":"TNT Run", - "estimated_time": "1-4 mins", "inspiration": "Grian (2013)", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Restez en mouvement, les blocs\n"},{"text":"disparaissent sous vos pieds !\n"}], - "en": [{"text":"Keep moving, blocks disappear\n"},{"text":"under your feet!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"sheepwars", "name_fr":"SheepWars", - "estimated_time": "2-6 mins", "inspiration": "Épicube", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Lancez des moutons explosifs\n"},{"text":"sur l'équipe ennemie !\n"}], - "en": [{"text":"Launch explosive sheep\n"},{"text":"at the enemy team!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"block_party", "name_fr":"Block Party", - "estimated_time": "1-5 mins", "inspiration": "HiveMC", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Quand la musique s'arrête,\n"},{"text":"trouvez la bonne couleur !\n"}], - "en": [{"text":"When the music stops,\n"},{"text":"find the right color!\n"}] - }, - }, - { - "min_players":2, "max_players":16, "id":"moutron", "name_fr":"Moutron", - "estimated_time": "1-3 mins", "inspiration": "Épicube", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Montez un mouton qui laisse\n"},{"text":"une traînée mortelle !\n"}], - "en": [{"text":"Ride a sheep that leaves\n"},{"text":"a deadly trail!\n"}] - }, - }, - { - "min_players":2, "max_players":45, "id":"snowball_painter", "name_fr":"Snowball Painter", - "estimated_time": "45s", "inspiration": "Party Games/Hypixel", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Peignez le plus de blocs possible\n"},{"text":"avec vos boules de neige !\n"}], - "en": [{"text":"Paint the most blocks possible\n"},{"text":"with your snowballs!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"gay_shooter", "name_fr":"Guy Shooter", - "estimated_time": "1-5 mins", "inspiration": "a Survisland Event", "suggested_by": "GoldVision & scartvol", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Archers contre grimpeurs :\n"},{"text":"empêchez-les d'atteindre le sommet !\n"}], - "en": [{"text":"Archers versus climbers:\n"},{"text":"prevent them from reaching the top!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"build_battle", "name_fr":"Build Battle", - "estimated_time": "5-7 mins", "inspiration": "Hypixel", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Construisez sur un thème\n"},{"text":"et votez pour le meilleur build !\n"}], - "en": [{"text":"Build on a theme and\n"},{"text":"vote for the best build!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"beat_the_kings", "name_fr":"Beat The Kings", - "estimated_time": "3-5 mins", "inspiration": "Henry IV/Louis XVI", "suggested_by": "Luxio", "developed_by": "Luxio", - "description": { - "fr": [{"text":"Révolutionnaires contre rois :\n"},{"text":"qui survivra à la bataille ?\n"}], - "en": [{"text":"Revolutionaries versus kings:\n"},{"text":"who will survive the battle?\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"coin_chaser", "name_fr":"Coin Chaser", - "estimated_time": "1m30s", "inspiration": "Expiration", "suggested_by": "Ikinox", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Collectez le plus d'or possible\n"},{"text":"avec vos élytras !\n"}], - "en": [{"text":"Collect the most gold possible\n"},{"text":"with your elytra!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"murder_mystery", "name_fr":"Murder Mystery", - "estimated_time": "1-4 mins", "inspiration": "Hypixel", "suggested_by": "GoldVision", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Un meurtrier, un détective,\n"},{"text":"et des innocents dans une map pleine d'or !\n"}], - "en": [{"text":"A murderer, a detective,\n"},{"text":"and innocents in a map full of gold!\n"}] - }, - }, - { - "min_players":4, "max_players":-1, "id":"panic_chase", "name_fr":"Panic Chase", - "estimated_time": "1-2 mins", "inspiration": "Le chat et la souris", "suggested_by": "Arti", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Survivez aux chasseurs dans\n"},{"text":"une petite arène fermée !\n"}], - "en": [{"text":"Survive the hunters in\n"},{"text":"a small closed arena!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"simultaneous_jump", "group":"a_coudre", "name_fr":"Simultaneous Jump", - "estimated_time": "60s", "inspiration": "Dé à Coudre/MLG à Coudre", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Dé à coudre ou MLG à coudre,\n"},{"text":"mais tout le monde saute en même temps !\n"}], - "en": [{"text":"Pool jump or MLG water bucket,\n"},{"text":"but everyone jumps at the same time!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"bombardement", "name_fr":"Bombardment", - "estimated_time": "1-2 mins", "inspiration": "Hypixel", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Esquivez les tirs de ce bâteau volant\n"},{"text":"afin d'être le dernier survivant !\n"}], - "en": [{"text":"Dodge the shots from this flying boat\n"},{"text":"to be the last survivor!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"shoot_da_sheep", "name_fr":"Shoot Da'Sheep", - "estimated_time": "60s", "inspiration": "BrawlMC", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Des moutons tombent du ciel, certains donnent plus de points\n"},{"text":"que d'autres, ayez le plus de points à la fin !\n"}], - "en": [{"text":"Sheeps are falling from the sky, some give more points\n"},{"text":"than others, get the most points at the end!\n"}] - }, - }, - { - "min_players":1, "max_players":-1, "id":"minigolf", "name_fr":"MiniGolf", - "estimated_time": "1-4 mins", "inspiration": "The physical world", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Soyez celui qui réussi à faire tomber sa balle dans\n"},{"text":"le trou avec le moins de coup possible !\n"}], - "en": [{"text":"Be the one to drop your golf ball in\n"},{"text":"the hole with the fewest strokes possible!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"cigogne", "name_fr":"Cigogne", - "estimated_time": "1-4 mins", "inspiration": "ShoupZ/JustNoob", "suggested_by": "GoldVision", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Combat de spectres puissants\n"},{"text":"dans une salle fermée !\n"}], - "en": [{"text":"Powerful spectres battle\n"},{"text":"in a closed room!\n"}] - }, - }, - { - "min_players":8, "max_players":-1, "id":"infected", "name_fr":"Infected", - "estimated_time": "5-10 mins", "inspiration": "ImagineYourCraft (2013)", "suggested_by": "the whole Earth", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Un infecté est choisi en début de partie et doit infecter\n"},{"text":"tous les humains qui doivent attendre les renforts !\n"}], - "en": [{"text":"An infected person is chosen at the beginning of the game and\n"},{"text":"must infect all humans who must wait for rescue!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"glassrunner", "group":"ctpf", "name_fr":"GlassRunner", - "estimated_time": "8-10 mins", "inspiration": "Épicube", "suggested_by": "AirDox", "developed_by": "AirDox", - "description": { - "fr": [{"text":"Affrontez l'équipe adverse dans le ciel dans un\n"},{"text":"CaptureThePoint assaisonné de multiples particularités\n"}], - "en": [{"text":"Take on the opposing team in the sky in a\n"},{"text":"CaptureThePoint seasoned with multiple special features\n"}] - }, - }, - { - "min_players":10, "max_players":-1, "id":"capture_the_flag", "group":"ctpf", "name_fr":"Capture The Flag", - "estimated_time": "10-15 mins", "inspiration": "Aspiration", "suggested_by": "Oraclette", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Deux équipes doivent s'affronter pour capturer\n"},{"text":"le drapeau ennemi grâce à des\n"},{"text":"classes spécifiques différentes\n"}], - "en": [{"text":"Two teams must compete to capture the enemy flag\n"},{"text":"with the help of different specific classes\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"memory_mine", "name_fr":"Memory Mine", - "estimated_time": "1-2 mins", "inspiration": "Pico Party", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Mémorisez la position des mines\n"},{"text":"qui clignotent et disparaissent !\n"}], - "en": [{"text":"Memorize the position of mines\n"},{"text":"that blink and disappear!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"fish_fight", "name_fr":"Fish Fight", - "estimated_time": "2-4 mins", "inspiration": "Findus", "suggested_by": "Luxio", "developed_by": "Luxio", - "description": { - "fr": [{"text":"Éjectez vos adversaires\n"},{"text":"avec votre poisson !\n"}], - "en": [{"text":"Eject your opponents\n"},{"text":"with your fish!\n"}] - }, - }, - { - "min_players":2, "max_players":10, "id":"one_shot", "name_fr":"One Shot", - "estimated_time": "2-4 mins", "inspiration": "OneShot", "suggested_by": "Luxio", "developed_by": "Luxio", - "description": { - "fr": [{"text":"Éliminez vos adversaires grâce à votre arc\n"},{"text":"surpuissant et votre épée pour remporter la victoire !\n"}], - "en": [{"text":"Eliminate your opponents with your\n"},{"text":"powerful bow and sword to claim victory!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"pillars_of_fortune", "name_fr":"Pillars of Fortune", - "estimated_time": "1-6 mins", "inspiration": "Cubecraft", "suggested_by": "TreekoZ", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Recevez des objets aléatoires\n"},{"text":"sur de grands piliers !\n"}], - "en": [{"text":"Receive random items\n"},{"text":"on large pillars!\n"}] - }, - }, - { - "min_players":2, "max_players":-1, "id":"fireblast", "name_fr":"Fireblast", - "estimated_time": "1-2 mins", "inspiration": "LifeCraft", "suggested_by": "Stoupy", "developed_by": "Stoupy", - "description": { - "fr": [{"text":"Éjectez tous les joueurs de la plateforme\n"},{"text":"en utilisant des boules de feu explosives !\n"}], - "en": [{"text":"Knock out every players from the platform\n"},{"text":"by using explosive fireballs!\n"}] - }, - }, - # { - # "min_players":2, "max_players":32, "id":"pvpswap", "name_fr":"PvPSwap", - # "estimated_time": "1-2 mins", "inspiration": "Épicube", "suggested_by": "Arobaze", "developed_by": "Arobaze", - # "description": { - # "fr": [{"text":"Éliminez les autres joueurs au travers\n"},{"text":"des changements inopinés de positions !\n"}], - # "en": [{"text":"Eliminate other players through\n"},{"text":"sudden changes in positions!\n"}] - # }, - # } -] - -# Voting groups: games sharing the same "group" key show up as a single vote entry -# (a game without a "group" key gets its own group, defaulting to its id). -# When a group wins the vote, a second vote is launched between its games (or the game is launched directly if only one fits the player count). -GROUP_NAME_COLOR: str = "#FFCC00" # Between yellow and gold, distinguishes multi-game groups in the vote message -GROUPS_INFO: dict[str, JsonDict] = { - "a_coudre": { - "name_fr": "Dé À Coudre & Co", - "estimated_time": "1-6 mins", - "description": { - "fr": [{"text":"Sautez dans la piscine, avec ou sans seau d'eau,\n"},{"text":"tous en même temps ou chacun son tour !\n"}], - "en": [{"text":"Jump into the pool, with or without a water bucket,\n"},{"text":"all at once or one at a time!\n"}] - }, - }, - "creepers": { - "name_fr": "Creepers", - "estimated_time": "1m30s", - "description": { - "fr": [{"text":"Survivez aux creepers, en les repoussant\n"},{"text":"ou en fuyant leurs explosions !\n"}], - "en": [{"text":"Survive the creepers, by pushing them away\n"},{"text":"or fleeing their explosions!\n"}] - }, - }, - "layers": { - "name_fr": "Layers", - "estimated_time": "4-10 mins", - "description": { - "fr": [{"text":"Équipez-vous rapidement et détruisez\n"},{"text":"les équipes adverses !\n"}], - "en": [{"text":"Gear up quickly and destroy\n"},{"text":"the enemy teams!\n"}] - }, - }, - "ctpf": { - "name_fr": "Capture the Point/Flag", - "estimated_time": "5-15 mins", - "description": { - "fr": [{"text":"Affrontez l'équipe adverse : capturez des points,\n"},{"text":"volez des drapeaux ou courez sur le verre !\n"}], - "en": [{"text":"Face off against the enemy team: capture points,\n"},{"text":"steal flags or run across the glass!\n"}] - }, - }, -} +from .spec import UNLIMITED, GroupSpec, Localized, ModeSpec + +# Constants +GROUP_NAME_COLOR: str = "#FFCC00" +""" Between yellow and gold, distinguishes multi-game groups in the vote message. """ -# Language pretext constant LANG_PRETEXT: dict[str, dict[str, str]] = { "fr": {"estimated_time": "Estimation :", "inspiration": "Inspiration :", "suggested": "Proposé par", "developed": "Développé par"}, "en": {"estimated_time": "Estimated:", "inspiration": "Inspiration:", "suggested": "Suggested by", "developed": "Developed by"}, } +""" Lore labels for a single mode, per language. """ -# Language pretext constant for group lores LANG_GROUP_PRETEXT: dict[str, dict[str, str]] = { "fr": {"includes": "Contient :", "second_vote": "Un second vote départagera la variante"}, "en": {"includes": "Includes:", "second_vote": "A second vote will decide the variant"}, } +""" Extra lore labels for a multi-mode group, per language. """ + + +# Minigames +MODES: tuple[ModeSpec, ...] = ( + ModeSpec( + id="pitch_creep", name_fr="Pitch Creep", group="creepers", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Pitchout/Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez en repoussant les creepers", "dans une arène fermée."), + en=("Survive by pushing away creepers", "in a closed arena."), + ), + ), + ModeSpec( + id="laser_game", name_fr="Laser Game", + min_players=2, max_players=UNLIMITED, + estimated_time="2m30s", inspiration="The physical world", suggested_by="Vigo", developed_by="Stoupy", + description=Localized( + fr=( + "Affrontez l'équipe adverse grâce à vos fusils-laser et faites", + "le plus d'élimination possible pendant un temps imparti !", + ), + en=( + "Battle the enemy team with your laser rifles and get", + "as many takedowns as possible within the allotted time!", + ), + ), + ), + ModeSpec( + id="warden_escape", name_fr="Warden Escape", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Minecraft 1.19", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux wardens dans", "une arène fermée."), + en=("Survive the wardens in", "a closed arena."), + ), + ), + ModeSpec( + id="pitchout", name_fr="Pitchout", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Poussez vos adversaires dans l'eau", "et soyez le dernier survivant !"), + en=("Push your opponents into the water", "and be the last survivor!"), + ), + ), + ModeSpec( + id="creeper_apocalypse", name_fr="Creeper Apocalypse", group="creepers", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Expiration", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux creepers qui se", "multiplient et explosent."), + en=("Survive the creepers that", "duplicate and explode."), + ), + ), + ModeSpec( + id="traitors_game", name_fr="Traitors Game", + min_players=4, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="Taupe Gun UHC", suggested_by="Luxio", developed_by="Stoupy", + description=Localized( + fr=( + "Des traitres se cachent parmi les joueurs.", + "Innocents contre Traitres, quel camp va-t-il gagner ?", + ), + en=("Traitors are hinding among players.", "Innocents versus Traitors, which side will win?"), + ), + ), + # ModeSpec( + # id="boat_race", name_fr="Boat Race", + # min_players=1, max_players=UNLIMITED, + # estimated_time="3-8 mins", inspiration="Expiration", suggested_by="Stoupy", developed_by="LTHCTheMaster", + # description=Localized( + # fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), + # en=("Race for reaching the finish line", "in first to claim the win!"), + # ), + # ), + ModeSpec( + id="spectres_game", name_fr="Spectres Game", + min_players=2, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="Expiration", suggested_by="Luxio", developed_by="Stoupy", + description=Localized( + fr=( + "Deux camps : un avec que des joueurs visibles,", + "l'autre avec des spectres, quel camp va-t-il gagner ?", + ), + en=("Two sides: one with visible players,", "the other with spectres, which side will win?"), + ), + ), + ModeSpec( + id="race", name_fr="Race", + min_players=1, max_players=UNLIMITED, + estimated_time="3-6 mins", inspiration="Racing games", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), + en=("Race for reaching the finish line", "in first to claim the win!"), + ), + ), + ModeSpec( + id="rush_the_point", name_fr="Rush The Point", group="ctpf", + min_players=8, max_players=UNLIMITED, + estimated_time="7-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Capturez les points centraux dans le vide", "avec vos classes spéciales."), + en=("Capture the central points in the void", "with your special classes."), + ), + ), + ModeSpec( + id="rush_the_flag", name_fr="Rush The Flag", group="ctpf", + min_players=8, max_players=UNLIMITED, + estimated_time="5-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Volez le drapeau ennemi", "avec vos classes spéciales."), + en=("Steal the enemy flag", "with your special classes."), + ), + ), + ModeSpec( + id="spleef", name_fr="Spleef", + min_players=1, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="everywhere", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Creusez sous vos adversaires", "pour les faire tomber !"), + en=("Dig below your opponents", "to make them fall!"), + ), + ), + ModeSpec( + id="de_a_coudre", name_fr="Dé à Coudre", group="a_coudre", + min_players=1, max_players=UNLIMITED, + estimated_time="3-6 mins", inspiration="OneCube point system", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Affrontez les autres joueurs dans un saut,", "dans une piscine, iconique de Minecraft !"), + en=("Take on the other players in", "Minecraft's iconic pool jump!"), + ), + ), + ModeSpec( + id="feed_fast", name_fr="Feed Fast", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="Le diabète", suggested_by="Arobaze", developed_by="LTHCTheMaster", + description=Localized( + fr=("Mangez le plus de nourriture", "possible pour gagner !"), + en=("Eat the most food", "possible to win!"), + ), + ), + ModeSpec( + id="mlg_a_coudre", name_fr="MLG à Coudre", group="a_coudre", + min_players=1, max_players=8, + estimated_time="1-6 mins", inspiration="Dé à Coudre", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Sautez et utilisez votre seau d'eau", "pour survivre à la chute !"), + en=("Jump and use your water bucket", "to survive the fall!"), + ), + ), + ModeSpec( + id="protect_the_king", name_fr="Protect The King", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Charlemagne", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Tuez le roi ennemi ou", "protégez le vôtre !"), + en=("Kill the enemy king or", "protect your own!"), + ), + ), + ModeSpec( + id="castagne", name_fr="Castagne RP", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="Domestic Violence", suggested_by="JustNoob", developed_by="Stoupy", + description=Localized( + fr=("Battez-vous au corps à corps", "et soyez le dernier survivant !"), + en=("Fight hand-to-hand combat", "and be the last survivor!"), + ), + ), + ModeSpec( + id="layers_2_teams", name_fr="Layers 2 Teams", group="layers", + min_players=2, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Équipez-vous rapidement et détruisez", "l'équipe adverse située de l'autre côté du mur !"), + en=("Gear up quickly and destroy the enemy team", "on the other side of the wall!"), + ), + ), + ModeSpec( + id="layers_4_teams", name_fr="Layers 4 Teams", group="layers", + min_players=4, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Course d'équipement entre", "4 équipes, éliminez vos ennemis !"), + en=("Equipment race between", "4 teams, eliminate your enemies!"), + ), + ), + ModeSpec( + id="thunder_spear", name_fr="Thunder Spear", + min_players=2, max_players=UNLIMITED, + estimated_time="2m30s", inspiration="SNK/Attack On Titan", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Utilisez votre équipement tridimensionnel", "et vos lances explosives !"), + en=("Use your three-dimensional maneuver gear", "and explosive spears!"), + ), + ), + ModeSpec( + id="tnt_run", name_fr="TNT Run", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Grian (2013)", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Restez en mouvement, les blocs", "disparaissent sous vos pieds !"), + en=("Keep moving, blocks disappear", "under your feet!"), + ), + ), + ModeSpec( + id="sheepwars", name_fr="SheepWars", + min_players=2, max_players=UNLIMITED, + estimated_time="2-6 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Lancez des moutons explosifs", "sur l'équipe ennemie !"), + en=("Launch explosive sheep", "at the enemy team!"), + ), + ), + ModeSpec( + id="block_party", name_fr="Block Party", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="HiveMC", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Quand la musique s'arrête,", "trouvez la bonne couleur !"), + en=("When the music stops,", "find the right color!"), + ), + ), + ModeSpec( + id="moutron", name_fr="Moutron", + min_players=2, max_players=16, + estimated_time="1-3 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Montez un mouton qui laisse", "une traînée mortelle !"), + en=("Ride a sheep that leaves", "a deadly trail!"), + ), + ), + ModeSpec( + id="snowball_painter", name_fr="Snowball Painter", + min_players=2, max_players=45, + estimated_time="45s", inspiration="Party Games/Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Peignez le plus de blocs possible", "avec vos boules de neige !"), + en=("Paint the most blocks possible", "with your snowballs!"), + ), + ), + ModeSpec( + id="gay_shooter", name_fr="Guy Shooter", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="a Survisland Event", suggested_by="GoldVision & scartvol", developed_by="Stoupy", + description=Localized( + fr=("Archers contre grimpeurs :", "empêchez-les d'atteindre le sommet !"), + en=("Archers versus climbers:", "prevent them from reaching the top!"), + ), + ), + ModeSpec( + id="build_battle", name_fr="Build Battle", + min_players=2, max_players=UNLIMITED, + estimated_time="5-7 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Construisez sur un thème", "et votez pour le meilleur build !"), + en=("Build on a theme and", "vote for the best build!"), + ), + ), + ModeSpec( + id="beat_the_kings", name_fr="Beat The Kings", + min_players=2, max_players=UNLIMITED, + estimated_time="3-5 mins", inspiration="Henry IV/Louis XVI", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=("Révolutionnaires contre rois :", "qui survivra à la bataille ?"), + en=("Revolutionaries versus kings:", "who will survive the battle?"), + ), + ), + ModeSpec( + id="coin_chaser", name_fr="Coin Chaser", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Expiration", suggested_by="Ikinox", developed_by="Stoupy", + description=Localized( + fr=("Collectez le plus d'or possible", "avec vos élytras !"), + en=("Collect the most gold possible", "with your elytra!"), + ), + ), + ModeSpec( + id="murder_mystery", name_fr="Murder Mystery", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Hypixel", suggested_by="GoldVision", developed_by="Stoupy", + description=Localized( + fr=("Un meurtrier, un détective,", "et des innocents dans une map pleine d'or !"), + en=("A murderer, a detective,", "and innocents in a map full of gold!"), + ), + ), + ModeSpec( + id="panic_chase", name_fr="Panic Chase", + min_players=4, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Le chat et la souris", suggested_by="Arti", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux chasseurs dans", "une petite arène fermée !"), + en=("Survive the hunters in", "a small closed arena!"), + ), + ), + ModeSpec( + id="simultaneous_jump", name_fr="Simultaneous Jump", group="a_coudre", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="Dé à Coudre/MLG à Coudre", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Dé à coudre ou MLG à coudre,", "mais tout le monde saute en même temps !"), + en=("Pool jump or MLG water bucket,", "but everyone jumps at the same time!"), + ), + ), + ModeSpec( + id="bombardement", name_fr="Bombardment", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Esquivez les tirs de ce bâteau volant", "afin d'être le dernier survivant !"), + en=("Dodge the shots from this flying boat", "to be the last survivor!"), + ), + ), + ModeSpec( + id="shoot_da_sheep", name_fr="Shoot Da'Sheep", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="BrawlMC", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=( + "Des moutons tombent du ciel, certains donnent plus de points", + "que d'autres, ayez le plus de points à la fin !", + ), + en=( + "Sheeps are falling from the sky, some give more points", + "than others, get the most points at the end!", + ), + ), + ), + ModeSpec( + id="minigolf", name_fr="MiniGolf", + min_players=1, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="The physical world", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Soyez celui qui réussi à faire tomber sa balle dans", "le trou avec le moins de coup possible !"), + en=("Be the one to drop your golf ball in", "the hole with the fewest strokes possible!"), + ), + ), + ModeSpec( + id="cigogne", name_fr="Cigogne", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="ShoupZ/JustNoob", suggested_by="GoldVision", developed_by="Stoupy", + description=Localized( + fr=("Combat de spectres puissants", "dans une salle fermée !"), + en=("Powerful spectres battle", "in a closed room!"), + ), + ), + ModeSpec( + id="infected", name_fr="Infected", + min_players=8, max_players=UNLIMITED, + estimated_time="5-10 mins", inspiration="ImagineYourCraft (2013)", suggested_by="the whole Earth", developed_by="Stoupy", + description=Localized( + fr=( + "Un infecté est choisi en début de partie et doit infecter", + "tous les humains qui doivent attendre les renforts !", + ), + en=( + "An infected person is chosen at the beginning of the game and", + "must infect all humans who must wait for rescue!", + ), + ), + ), + ModeSpec( + id="glassrunner", name_fr="GlassRunner", group="ctpf", + min_players=2, max_players=UNLIMITED, + estimated_time="8-10 mins", inspiration="Épicube", suggested_by="AirDox", developed_by="AirDox", + description=Localized( + fr=( + "Affrontez l'équipe adverse dans le ciel dans un", + "CaptureThePoint assaisonné de multiples particularités", + ), + en=( + "Take on the opposing team in the sky in a", + "CaptureThePoint seasoned with multiple special features", + ), + ), + ), + ModeSpec( + id="capture_the_flag", name_fr="Capture The Flag", group="ctpf", + min_players=10, max_players=UNLIMITED, + estimated_time="10-15 mins", inspiration="Aspiration", suggested_by="Oraclette", developed_by="Stoupy", + description=Localized( + fr=( + "Deux équipes doivent s'affronter pour capturer", + "le drapeau ennemi grâce à des", + "classes spécifiques différentes", + ), + en=("Two teams must compete to capture the enemy flag", "with the help of different specific classes"), + ), + ), + ModeSpec( + id="memory_mine", name_fr="Memory Mine", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Pico Party", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Mémorisez la position des mines", "qui clignotent et disparaissent !"), + en=("Memorize the position of mines", "that blink and disappear!"), + ), + ), + ModeSpec( + id="fish_fight", name_fr="Fish Fight", + min_players=2, max_players=UNLIMITED, + estimated_time="2-4 mins", inspiration="Findus", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=("Éjectez vos adversaires", "avec votre poisson !"), + en=("Eject your opponents", "with your fish!"), + ), + ), + ModeSpec( + id="one_shot", name_fr="One Shot", + min_players=2, max_players=10, + estimated_time="2-4 mins", inspiration="OneShot", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=( + "Éliminez vos adversaires grâce à votre arc", + "surpuissant et votre épée pour remporter la victoire !", + ), + en=("Eliminate your opponents with your", "powerful bow and sword to claim victory!"), + ), + ), + ModeSpec( + id="pillars_of_fortune", name_fr="Pillars of Fortune", + min_players=2, max_players=UNLIMITED, + estimated_time="1-6 mins", inspiration="Cubecraft", suggested_by="TreekoZ", developed_by="Stoupy", + description=Localized( + fr=("Recevez des objets aléatoires", "sur de grands piliers !"), + en=("Receive random items", "on large pillars!"), + ), + ), + ModeSpec( + id="fireblast", name_fr="Fireblast", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="LifeCraft", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Éjectez tous les joueurs de la plateforme", "en utilisant des boules de feu explosives !"), + en=("Knock out every players from the platform", "by using explosive fireballs!"), + ), + ), + # ModeSpec( + # id="pvpswap", name_fr="PvPSwap", + # min_players=2, max_players=32, + # estimated_time="1-2 mins", inspiration="Épicube", suggested_by="Arobaze", developed_by="Arobaze", + # description=Localized( + # fr=("Éliminez les autres joueurs au travers", "des changements inopinés de positions !"), + # en=("Eliminate other players through", "sudden changes in positions!"), + # ), + # ), +) + +# Vote groups: modes sharing a "group" show up as a single entry, and a second vote picks one +GROUPS: tuple[GroupSpec, ...] = ( + GroupSpec( + id="a_coudre", name_fr="Dé À Coudre & Co", estimated_time="1-6 mins", + description=Localized( + fr=("Sautez dans la piscine, avec ou sans seau d'eau,", "tous en même temps ou chacun son tour !"), + en=("Jump into the pool, with or without a water bucket,", "all at once or one at a time!"), + ), + ), + GroupSpec( + id="creepers", name_fr="Creepers", estimated_time="1m30s", + description=Localized( + fr=("Survivez aux creepers, en les repoussant", "ou en fuyant leurs explosions !"), + en=("Survive the creepers, by pushing them away", "or fleeing their explosions!"), + ), + ), + GroupSpec( + id="layers", name_fr="Layers", estimated_time="4-10 mins", + description=Localized( + fr=("Équipez-vous rapidement et détruisez", "les équipes adverses !"), + en=("Gear up quickly and destroy", "the enemy teams!"), + ), + ), + GroupSpec( + id="ctpf", name_fr="Capture the Point/Flag", estimated_time="5-15 mins", + description=Localized( + fr=("Affrontez l'équipe adverse : capturez des points,", "volez des drapeaux ou courez sur le verre !"), + en=("Face off against the enemy team: capture points,", "steal flags or run across the glass!"), + ), + ), +) + +GROUPS_BY_ID: dict[str, GroupSpec] = {group.id: group for group in GROUPS} +""" Lookup used when building the storage entry of a multi-mode group. """ + + +# Functions +def mode_lore(mode: ModeSpec, lang: str) -> list[TextComponent]: + """ Build the vote lore of one mode: title, description lines, then the four credit lines. + + Args: + mode (ModeSpec): The mode to describe + lang (str): Language key, ex: "fr" or "en" + Returns: + list[TextComponent]: The lore, ready to be stored + """ + pretext: dict[str, str] = LANG_PRETEXT[lang] + description: tuple[str, ...] = mode.description.fr if lang == "fr" else mode.description.en + return [ + "", + {"text": f"[{mode.name_fr}]\n", "color": "yellow"}, + *({"text": f"{line}\n"} for line in description), + {"text": f"\n[{pretext['estimated_time']} {mode.estimated_time}]", "color": "gold"}, + {"text": f"\n[{pretext['inspiration']} {mode.inspiration}]", "color": "green"}, + {"text": f"\n[{pretext['suggested']} {mode.suggested_by}]", "color": "aqua"}, + {"text": f"\n[{pretext['developed']} {mode.developed_by}]", "color": "aqua"}, + ] + + +def group_lore(group: GroupSpec, modes: tuple[ModeSpec, ...], lang: str) -> list[TextComponent]: + """ Build the vote lore of a multi-mode group: title, description, duration, members, hint. + + Args: + group (GroupSpec): The group to describe + modes (tuple[ModeSpec...]): Its members, in vote order + lang (str): Language key, ex: "fr" or "en" + Returns: + list[TextComponent]: The lore, ready to be stored + """ + pretext: dict[str, str] = LANG_PRETEXT[lang] + group_pretext: dict[str, str] = LANG_GROUP_PRETEXT[lang] + description: tuple[str, ...] = group.description.fr if lang == "fr" else group.description.en + included: str = ", ".join(mode.name_fr for mode in modes) + return [ + "", + {"text": f"[{group.name_fr}]\n", "color": GROUP_NAME_COLOR}, + *({"text": f"{line}\n"} for line in description), + {"text": f"\n[{pretext['estimated_time']} {group.estimated_time}]", "color": "gold"}, + {"text": f"\n[{group_pretext['includes']} {included}]", "color": "green"}, + {"text": f"\n[{group_pretext['second_vote']}]", "color": "aqua"}, + ] + + +def storage_max_players(max_players: int | None) -> int: + """ Storage form of a player cap, where unlimited is -1. -# Add the lores to the modes -def add_lore(): - for i, mode in enumerate(MODES): - description: dict[str, list[TextComponent]] = mode["description"] - - # Add the missing lore for each language - for lang, lore in description.items(): - pretext: dict[str, str] = LANG_PRETEXT[lang] - lore.insert(0, "") - lore.insert(1, {"text":f"[{mode['name_fr']}]\n","color":"yellow"}) - lore.append({"text":f"\n[{pretext['estimated_time']} {mode['estimated_time']}]","color":"gold"}) - lore.append({"text":f"\n[{pretext['inspiration']} {mode['inspiration']}]","color":"green"}) - lore.append({"text":f"\n[{pretext['suggested']} {mode['suggested_by']}]","color":"aqua"}) - lore.append({"text":f"\n[{pretext['developed']} {mode['developed_by']}]","color":"aqua"}) - lore_key: str = f"lore_{lang}" - mode[lore_key] = lore - - # Add keys for index and index*100 (utility for things such as /rating, /coupdetat, etc.) - mode["index"] = i + 1 - mode["index_hundred"] = (i + 1) * 100 - - # Colored name shown in the vote message: the color must be baked in the stored component because - # the outer style of an interpreted nbt component overrides the parsed one (ComponentUtils.resolve) - mode["display_name_fr"] = {"text": mode["name_fr"], "color": "yellow"} - mode["display_name_en"] = {"text": mode.get("name_en", mode["name_fr"]), "color": "yellow"} - - # Default the voting group to the game id (games sharing a group are voted as one entry) - mode.setdefault("group", mode["id"]) - - del mode["description"] - -def get_groups() -> list[JsonDict]: - """ Build the voting groups list from MODES (must be called after add_lore()). """ - members: dict[str, list[JsonDict]] = {} + Args: + max_players (int | None): The cap, or UNLIMITED + Returns: + int: The stored value + + Examples: + >>> storage_max_players(UNLIMITED) + -1 + >>> storage_max_players(8) + 8 + """ + return -1 if max_players is None else max_players + + +def mode_entry(mode: ModeSpec, index: int) -> JsonDict: + """ Storage entry of one mode, appended to the "minigames" list. + + Key insertion order is part of the generated output: an explicit group sits right after the + id, a defaulted one is added last. Reordering rewrites every function file for nothing. + + Args: + mode (ModeSpec): The mode to store + index (int): Its 1-based rank, used by /rating and /coupdetat + Returns: + JsonDict: The entry + """ + entry: JsonDict = { + "min_players": mode.min_players, + "max_players": storage_max_players(mode.max_players), + "id": mode.id, + } + if mode.group is not None: + entry["group"] = mode.group + entry["name_fr"] = mode.name_fr + entry["estimated_time"] = mode.estimated_time + entry["inspiration"] = mode.inspiration + entry["suggested_by"] = mode.suggested_by + entry["developed_by"] = mode.developed_by + entry["lore_fr"] = mode_lore(mode, "fr") + entry["lore_en"] = mode_lore(mode, "en") + entry["index"] = index + entry["index_hundred"] = index * 100 + entry["display_name_fr"] = {"text": mode.display_name("fr"), "color": "yellow"} + entry["display_name_en"] = {"text": mode.display_name("en"), "color": "yellow"} + if mode.group is None: + entry["group"] = mode.id + return entry + + +def group_entry(group_id: str, modes: tuple[ModeSpec, ...], index: int) -> JsonDict: + """ Storage entry of one vote group, appended to the "groups" list. + + A group holding a single mode is the mode itself, so the first vote shows it directly. + + Args: + group_id (str): The group id, ex: "layers" + modes (tuple[ModeSpec...]): Its members, in vote order + index (int): Its 1-based rank + Returns: + JsonDict: The entry + """ + if len(modes) == 1: + mode: ModeSpec = modes[0] + return { + "min_players": mode.min_players, + "max_players": storage_max_players(mode.max_players), + "id": group_id, + "name_fr": mode.name_fr, + "name_en": mode.display_name("en"), + "display_name_fr": {"text": mode.display_name("fr"), "color": "yellow"}, + "display_name_en": {"text": mode.display_name("en"), "color": "yellow"}, + "lore_fr": mode_lore(mode, "fr"), + "lore_en": mode_lore(mode, "en"), + "index": index, + } + + group: GroupSpec = GROUPS_BY_ID[group_id] + bounded: list[int] = [mode.max_players for mode in modes if mode.max_players is not None] + return { + "min_players": min(mode.min_players for mode in modes), + "max_players": max(bounded) if len(bounded) == len(modes) else -1, + "id": group_id, + "name_fr": group.name_fr, + "name_en": group.display_name("en"), + "display_name_fr": {"text": group.name_fr, "color": GROUP_NAME_COLOR}, + "display_name_en": {"text": group.display_name("en"), "color": GROUP_NAME_COLOR}, + "lore_fr": group_lore(group, modes, "fr"), + "lore_en": group_lore(group, modes, "en"), + "index": index, + } + + +def grouped_modes() -> dict[str, tuple[ModeSpec, ...]]: + """ Modes bucketed by vote group, keeping the declaration order of MODES. + + Returns: + dict[str, tuple[ModeSpec, ...]]: Group id -> its members + """ + members: dict[str, list[ModeSpec]] = {} for mode in MODES: - members.setdefault(mode["group"], []).append(mode) - - groups: list[JsonDict] = [] - for i, (group_id, modes) in enumerate(members.items()): - if len(modes) == 1: - # Single game: the group entry is the game itself - mode = modes[0] - entry: JsonDict = { - "min_players": mode["min_players"], "max_players": mode["max_players"], "id": group_id, - "name_fr": mode["name_fr"], "name_en": mode.get("name_en", mode["name_fr"]), - "display_name_fr": mode["display_name_fr"], "display_name_en": mode["display_name_en"], - "lore_fr": mode["lore_fr"], "lore_en": mode["lore_en"], - } - else: - # Multiple games: aggregate player bounds and build a dedicated lore from GROUPS_INFO - # (the display_name carries GROUP_NAME_COLOR so the vote message distinguishes multi-game groups) - info: JsonDict = GROUPS_INFO[group_id] - entry = { - "min_players": min(mode["min_players"] for mode in modes), - "max_players": -1 if any(mode["max_players"] == -1 for mode in modes) else max(mode["max_players"] for mode in modes), - "id": group_id, - "name_fr": info["name_fr"], "name_en": info.get("name_en", info["name_fr"]), - "display_name_fr": {"text": info["name_fr"], "color": GROUP_NAME_COLOR}, - "display_name_en": {"text": info.get("name_en", info["name_fr"]), "color": GROUP_NAME_COLOR}, - } - included: str = ", ".join(mode["name_fr"] for mode in modes) - for lang in ("fr", "en"): - pretext: dict[str, str] = LANG_PRETEXT[lang] - group_pretext: dict[str, str] = LANG_GROUP_PRETEXT[lang] - entry[f"lore_{lang}"] = [ - "", - {"text":f"[{info['name_fr']}]\n","color":GROUP_NAME_COLOR}, - *info["description"][lang], - {"text":f"\n[{pretext['estimated_time']} {info['estimated_time']}]","color":"gold"}, - {"text":f"\n[{group_pretext['includes']} {included}]","color":"green"}, - {"text":f"\n[{group_pretext['second_vote']}]","color":"aqua"}, - ] - entry["index"] = i + 1 - groups.append(entry) - return groups - -def write_modes_load_file(): - ns: str = Mem.ctx.project_id - add_lore() - append_lines: list[str] = [f"data modify storage {ns}:main minigames append value " + json.dumps(mode, ensure_ascii=False) for mode in MODES] + members.setdefault(mode.vote_group, []).append(mode) + return {group_id: tuple(modes) for group_id, modes in members.items()} - # Voting groups list (what the first vote is made of) - append_lines.append("\n# Voting groups (the first vote shows groups, a second vote decides between the games of the winning group)") - append_lines.append(f"data modify storage {ns}:main groups set value []") - append_lines += [f"data modify storage {ns}:main groups append value " + json.dumps(group, ensure_ascii=False) for group in get_groups()] - write_function(f"{ns}:modes/load", "\n".join(append_lines)) +def write_modes_load_file() -> None: + """ Write switch:modes/load, which fills the minigames and groups storage lists. """ + ns: str = Mem.ctx.project_id + lines: list[str] = [ + f"data modify storage {ns}:main minigames append value " + json.dumps(mode_entry(mode, index), ensure_ascii=False) + for index, mode in enumerate(MODES, start=1) + ] + lines.append("\n# Voting groups (the first vote shows groups, a second vote decides between the games of the winning group)") + lines.append(f"data modify storage {ns}:main groups set value []") + lines += [ + f"data modify storage {ns}:main groups append value " + json.dumps(group_entry(group_id, modes, index), ensure_ascii=False) + for index, (group_id, modes) in enumerate(grouped_modes().items(), start=1) + ] + write_function(f"{ns}:modes/load", "\n".join(lines)) diff --git a/src/datapack/modes/one_shot/main.py b/src/datapack/modes/one_shot/main.py index fb435c21a..0986a71ff 100644 --- a/src/datapack/modes/one_shot/main.py +++ b/src/datapack/modes/one_shot/main.py @@ -1,4 +1,5 @@ +# ruff: noqa: E501 # Imports from stewbeet import Mem, write_function diff --git a/src/datapack/modes/rush_the_point/kits.py b/src/datapack/modes/rush_the_point/kits.py index baf3e2083..09a68de02 100644 --- a/src/datapack/modes/rush_the_point/kits.py +++ b/src/datapack/modes/rush_the_point/kits.py @@ -8,6 +8,7 @@ so the kits are built lazily by build_kits() instead of living at module level: the stewbeet CLI imports this module before Mem.ctx exists. """ +# ruff: noqa: E501 # Imports from stewbeet import Mem diff --git a/src/datapack/modes/spec.py b/src/datapack/modes/spec.py new file mode 100644 index 000000000..997567b43 --- /dev/null +++ b/src/datapack/modes/spec.py @@ -0,0 +1,117 @@ +""" Typed model of a minigame and of a vote group. + +Pure data: no beet, no stewbeet, no Mem. Everything the vote message, the lore and the starting +pop-ups need is declared here, and definitions.py turns it into storage entries. +""" +# Imports +from dataclasses import dataclass + +# Constants +UNLIMITED: None = None +""" Value of ModeSpec.max_players when a mode accepts any number of players. """ + + +# Classes +@dataclass(frozen=True, kw_only=True) +class Localized: + """ One short text in both supported languages, split into displayed lines. + + Lines are stored without their trailing newline: the emitter adds it back. + """ + + fr: tuple[str, ...] + """ French lines, ex: ("Survivez en repoussant les creepers", "dans une arène fermée") """ + + en: tuple[str, ...] + """ English lines, ex: ("Survive by pushing away creepers", "in a closed arena") """ + + +@dataclass(frozen=True, kw_only=True) +class ModeSpec: + """ Everything the engine needs to know about one minigame, outside of its logic. + + The id must match the folder name under src/datapack/modes/, which is what the build validates. + """ + + id: str + """ Folder name and storage id, ex: "tnt_run". Frozen forever: the stats objectives embed it. """ + + name_fr: str + """ Name shown in the vote message, ex: "TNT Run" """ + + name_en: str | None = None + """ English name when it differs from the French one, otherwise name_fr is used. """ + + min_players: int + """ Smallest player count that allows the mode to be voted. """ + + max_players: int | None = UNLIMITED + """ Largest player count, or UNLIMITED when the mode has no upper bound. """ + + group: str | None = None + """ Vote group this mode belongs to, or None when the mode is its own group. """ + + estimated_time: str + """ Human readable duration shown in the lore, ex: "1-4 mins" """ + + inspiration: str + """ Where the concept comes from, ex: "Grian (2013)" """ + + suggested_by: str + """ Who proposed the mode. """ + + developed_by: str + """ Who implemented the mode. """ + + description: Localized + """ Two or three lines pitching the mode, shown in the vote lore. """ + + @property + def vote_group(self) -> str: + """ The group this mode is voted under, defaulting to its own id. + + Returns: + str: The group id + """ + return self.group if self.group is not None else self.id + + def display_name(self, lang: str) -> str: + """ The mode name in the given language, falling back to French. + + Args: + lang (str): Language key, ex: "fr" or "en" + Returns: + str: The localized name + """ + return self.name_en if lang == "en" and self.name_en is not None else self.name_fr + + +@dataclass(frozen=True, kw_only=True) +class GroupSpec: + """ A vote entry gathering several minigames, shown before the second vote picks one. """ + + id: str + """ Group id, referenced by ModeSpec.group, ex: "layers" """ + + name_fr: str + """ Name shown in the vote message, ex: "Layers" """ + + name_en: str | None = None + """ English name when it differs from the French one, otherwise name_fr is used. """ + + estimated_time: str + """ Human readable duration covering every mode of the group, ex: "4-10 mins" """ + + description: Localized + """ Two lines pitching what the group has in common. """ + + def display_name(self, lang: str) -> str: + """ The group name in the given language, falling back to French. + + Args: + lang (str): Language key, ex: "fr" or "en" + Returns: + str: The localized name + """ + return self.name_en if lang == "en" and self.name_en is not None else self.name_fr + diff --git a/src/datapack/survival_maps/utils.py b/src/datapack/survival_maps/utils.py index 0bc4b81b7..2dfc612bf 100644 --- a/src/datapack/survival_maps/utils.py +++ b/src/datapack/survival_maps/utils.py @@ -791,7 +791,7 @@ def generate_map_usage_file() -> None: # Get for each modes the maps they use modes_usage: dict[str, list[str]] = {} for mode in MODES: - mode_id: str = mode["id"] + mode_id: str = mode.id start_file: str = f"{ns}:modes/{mode_id}/" modes_usage[mode_id] = [] From 50ccbfa373802c5294753e4f6c405b449c44df47 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:30:16 +0200 Subject: [PATCH 03/10] =?UTF-8?q?feat(build):=20=E2=9C=A8=20Validation=20d?= =?UTF-8?q?es=20modes,=20groupes=20et=20maps=20au=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/link.py | 4 + src/validation.py | 190 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/validation.py diff --git a/src/link.py b/src/link.py index cdcae7fcc..8b4883b8b 100644 --- a/src/link.py +++ b/src/link.py @@ -23,6 +23,7 @@ from .datapack.translations.main import main as shared_translations from .datapack.utils.main import main as utils from .resource_pack.special_fonts.main import main as special_fonts +from .validation import Validation # Main function is run just before making finalyzing the build process (zip, headers, lang, ...) @@ -63,6 +64,9 @@ def beet_default(ctx: Context) -> None: shop() special_fonts() + # Fail the build on a mode, group or map declaration that would only break in game + Validation.run() + # Force enable dump module from Bookshelf (https://docs.mcbookshelf.dev/en/latest/modules/dump.html) official_lib_used("bs.dump") diff --git a/src/validation.py b/src/validation.py new file mode 100644 index 000000000..8ac9531d3 --- /dev/null +++ b/src/validation.py @@ -0,0 +1,190 @@ +""" Build-time validation of the project's declarative data. + +Catches at build time what used to only show up in game: a mode without its folder, a group that +nobody declares, a map id with a typo. Every message names the offending declaration and, when it +can, the closest valid value. +""" +# Imports +import json +import re +from dataclasses import dataclass +from difflib import get_close_matches +from pathlib import Path + +import stouputils as stp +from stewbeet import Mem + +from .datapack.modes.definitions import GROUPS, GROUPS_BY_ID, MODES, grouped_modes +from .datapack.survival_maps.shared_memory import generated_maps + +# Constants +MODES_FOLDER: Path = Path(__file__).parent / "datapack" / "modes" +""" Folder holding one package per minigame, used to check that every spec has its code. """ + +RESERVED_FOLDERS: frozenset[str] = frozenset({"_common", "_coupdetat"}) +""" Mode folders that are helpers for the engine, never minigames of their own. """ + +MAPS_OUTSIDE_REGISTRY: frozenset[str] = frozenset({"build_battle", "kart_racer_relai"}) +""" Maps that live in the world instead of being generated by the survival map pipeline. + +They are the two maps without an intro cinematic, see the note in switch:engine/signals/second. +""" + +CHOOSE_MAP_PATTERN: re.Pattern[str] = re.compile(r"choose_map_for\s*\{[^}]*maps:(\[[^\]]*\])") +""" Captures the maps list of a `function switch:utils/choose_map_for {id:"x", maps:[...]}` call. """ + +MAP_CONDITION_PATTERN: re.Pattern[str] = re.compile(r'\{map:"([a-z0-9_]+)"\}') +""" Captures the map id of a `if data storage switch:main {map:"x"}` condition. """ + + +# Classes +@dataclass(frozen=True) +class Problem: + """ One validation failure, rendered as a single actionable line. """ + + message: str + """ What is wrong, naming the offending declaration. """ + + hint: str = "" + """ Optional suggestion, ex: the closest valid map id. """ + + def render(self) -> str: + """ The message with its hint appended when there is one. + + Returns: + str: The line to print + + Examples: + >>> Problem("mode 'x' has no folder").render() + "mode 'x' has no folder" + >>> Problem("unknown map 'tnt_runn'", "did you mean 'tnt_run'?").render() + "unknown map 'tnt_runn' (did you mean 'tnt_run'?)" + """ + return f"{self.message} ({self.hint})" if self.hint else self.message + + +class Validation: + + @staticmethod + def suggest(value: str, candidates: list[str]) -> str: + """ A "did you mean" hint for a value that was not found. + + Args: + value (str): The offending value, ex: "tnt_runn" + candidates (list[str]): Every valid value + Returns: + str: The hint, empty when nothing is close enough + + Examples: + >>> Validation.suggest("tnt_runn", ["tnt_run", "spleef"]) + "did you mean 'tnt_run'?" + >>> Validation.suggest("zzzzz", ["tnt_run"]) + '' + """ + close: list[str] = get_close_matches(value, candidates, n=1) + return f"did you mean '{close[0]}'?" if close else "" + + @staticmethod + def declared_folders() -> set[str]: + """ Mode folders holding a main.py, excluding the engine helpers. + + Returns: + set[str]: Folder names, ex: {"tnt_run", "spleef"} + """ + return {entry.name for entry in MODES_FOLDER.iterdir() + if entry.is_dir() and entry.name not in RESERVED_FOLDERS and (entry / "main.py").is_file()} + + @staticmethod + def check_modes() -> list[Problem]: + """ Every mode has a unique id, a folder, a sane player range and a declared group. + + Returns: + list[Problem]: The failures found + """ + problems: list[Problem] = [] + folders: set[str] = Validation.declared_folders() + seen: set[str] = set() + + for mode in MODES: + if mode.id in seen: + problems.append(Problem(f"mode '{mode.id}' is declared twice in MODES")) + seen.add(mode.id) + + if mode.id not in folders: + problems.append(Problem( + f"mode '{mode.id}' has no folder src/datapack/modes/{mode.id}/main.py", + Validation.suggest(mode.id, sorted(folders)))) + + if mode.min_players < 1: + problems.append(Problem(f"mode '{mode.id}' has min_players={mode.min_players}, expected 1 or more")) + if mode.max_players is not None and mode.max_players < mode.min_players: + problems.append(Problem( + f"mode '{mode.id}' has max_players={mode.max_players} below min_players={mode.min_players}")) + + for group_id, modes in grouped_modes().items(): + if len(modes) > 1 and group_id not in GROUPS_BY_ID: + problems.append(Problem( + f"group '{group_id}' gathers {len(modes)} modes but is missing from GROUPS", + Validation.suggest(group_id, sorted(GROUPS_BY_ID)))) + return problems + + @staticmethod + def referenced_maps() -> dict[str, set[str]]: + """ Every map id referenced by a generated function, mapped to the functions using it. + + Returns: + dict[str, set[str]]: Map id -> function paths + """ + referenced: dict[str, set[str]] = {} + for path, function in Mem.ctx.data.functions.items(): + text: str = function.text + names: list[str] = MAP_CONDITION_PATTERN.findall(text) + for raw in CHOOSE_MAP_PATTERN.findall(text): + names.extend(json.loads(raw)) + for name in names: + referenced.setdefault(name, set()).add(path) + return referenced + + @staticmethod + def check_maps() -> list[Problem]: + """ Every referenced map is either generated or a known world map. + + Returns: + list[Problem]: The failures found + """ + known: set[str] = set(generated_maps) | MAPS_OUTSIDE_REGISTRY + problems: list[Problem] = [] + for name, paths in sorted(Validation.referenced_maps().items()): + if name not in known: + culprit: str = sorted(paths)[0] + problems.append(Problem( + f"unknown map '{name}' referenced by {culprit}", + Validation.suggest(name, sorted(generated_maps)))) + return problems + + @staticmethod + def report_unvoted() -> None: + """ Log the modes that generate but never reach the vote, and the groups nobody uses. """ + voted: set[str] = {mode.id for mode in MODES} + unvoted: list[str] = sorted(Validation.declared_folders() - voted) + if unvoted: + stp.info(f"{len(unvoted)} mode(s) generated but absent from the vote: {', '.join(unvoted)}") + + used: set[str] = {group_id for group_id, modes in grouped_modes().items() if len(modes) > 1} + unused: list[str] = sorted(group.id for group in GROUPS if group.id not in used) + if unused: + stp.warning(f"{len(unused)} group(s) declared but unused: {', '.join(unused)}") + + @staticmethod + def run() -> None: + """ Run every check, then stop the build when something is wrong. + + Raises: + ValueError: When at least one check failed + """ + problems: list[Problem] = Validation.check_modes() + Validation.check_maps() + Validation.report_unvoted() + if problems: + listed: str = "\n".join(f" {problem.render()}" for problem in problems) + raise ValueError(f"{len(problems)} declaration problem(s):\n{listed}") + From f959b679de10d8cbfed6b2c43f7406da5d308892 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:31:33 +0200 Subject: [PATCH 04/10] =?UTF-8?q?refactor(modes):=20=E2=99=BB=EF=B8=8F=20c?= =?UTF-8?q?ommon.py=20renomm=C3=A9=20en=20emit.py=20(lev=C3=A9e=20d'ambigu?= =?UTF-8?q?=C3=AFt=C3=A9=20avec=20=5Fcommon/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/datapack/modes/_coupdetat/main.py | 2 +- src/datapack/modes/beat_the_kings/main.py | 2 +- src/datapack/modes/block_party/main.py | 2 +- src/datapack/modes/block_party/translations.py | 2 +- src/datapack/modes/boat_race/main.py | 2 +- src/datapack/modes/boat_race/translations.py | 2 +- src/datapack/modes/bombardement/main.py | 2 +- src/datapack/modes/bombardement/translations.py | 2 +- src/datapack/modes/build_battle/main.py | 2 +- src/datapack/modes/build_battle/resources.py | 2 +- src/datapack/modes/capture_the_flag/main.py | 2 +- src/datapack/modes/castagne/main.py | 2 +- src/datapack/modes/cigogne/main.py | 2 +- src/datapack/modes/coin_chaser/main.py | 2 +- src/datapack/modes/coin_chaser/translations.py | 2 +- src/datapack/modes/creeper_apocalypse/main.py | 2 +- src/datapack/modes/creeper_apocalypse/translations.py | 2 +- src/datapack/modes/de_a_coudre/main.py | 2 +- src/datapack/modes/{common.py => emit.py} | 0 src/datapack/modes/feed_fast/main.py | 2 +- src/datapack/modes/feed_fast/translations.py | 2 +- src/datapack/modes/fireblast/main.py | 2 +- src/datapack/modes/fireblast/translations.py | 2 +- src/datapack/modes/fish_fight/main.py | 2 +- src/datapack/modes/gay_shooter/main.py | 2 +- src/datapack/modes/gay_shooter/translations.py | 2 +- src/datapack/modes/glassrunner/main.py | 2 +- src/datapack/modes/glassrunner/resources.py | 2 +- src/datapack/modes/glassrunner/translations.py | 2 +- src/datapack/modes/infected/main.py | 2 +- src/datapack/modes/laser_game/main.py | 2 +- src/datapack/modes/laser_game/translations.py | 2 +- src/datapack/modes/layers_2_teams/main.py | 2 +- src/datapack/modes/layers_4_teams/main.py | 2 +- src/datapack/modes/memory_mine/main.py | 2 +- src/datapack/modes/memory_mine/translations.py | 2 +- src/datapack/modes/minigolf/main.py | 2 +- src/datapack/modes/minigolf/translations.py | 2 +- src/datapack/modes/mlg_a_coudre/main.py | 2 +- src/datapack/modes/mlg_a_coudre/translations.py | 2 +- src/datapack/modes/moutron/main.py | 2 +- src/datapack/modes/moutron/translations.py | 2 +- src/datapack/modes/murder_mystery/main.py | 2 +- src/datapack/modes/murder_mystery/translations.py | 2 +- src/datapack/modes/one_shot/main.py | 2 +- src/datapack/modes/panic_chase/main.py | 2 +- src/datapack/modes/pillars_of_fortune/main.py | 2 +- src/datapack/modes/pitch_creep/main.py | 2 +- src/datapack/modes/pitch_creep/translations.py | 2 +- src/datapack/modes/pitchout/main.py | 2 +- src/datapack/modes/pitchout/translations.py | 2 +- src/datapack/modes/protect_the_king/main.py | 2 +- src/datapack/modes/pvpswap/main.py | 2 +- src/datapack/modes/race/main.py | 2 +- src/datapack/modes/replicate_the_build/main.py | 2 +- src/datapack/modes/replicate_the_build/resources.py | 2 +- src/datapack/modes/rush_the_flag/main.py | 2 +- src/datapack/modes/rush_the_flag/resources.py | 2 +- src/datapack/modes/rush_the_point/main.py | 2 +- src/datapack/modes/rush_the_point/resources.py | 2 +- src/datapack/modes/sheepwars/main.py | 2 +- src/datapack/modes/shoot_da_sheep/main.py | 2 +- src/datapack/modes/shoot_da_sheep/translations.py | 2 +- src/datapack/modes/simultaneous_jump/main.py | 2 +- src/datapack/modes/simultaneous_jump/translations.py | 2 +- src/datapack/modes/snowball_painter/main.py | 2 +- src/datapack/modes/snowball_painter/translations.py | 2 +- src/datapack/modes/spectres_game/main.py | 2 +- src/datapack/modes/spleef/main.py | 2 +- src/datapack/modes/spleef/translations.py | 2 +- src/datapack/modes/thunder_spear/main.py | 2 +- src/datapack/modes/thunder_spear/translations.py | 2 +- src/datapack/modes/tnt_run/main.py | 2 +- src/datapack/modes/tnt_run/translations.py | 2 +- src/datapack/modes/traitors_game/main.py | 2 +- src/datapack/modes/warden_escape/main.py | 2 +- src/datapack/modes/warden_escape/resources.py | 2 +- src/datapack/modes/warden_escape/translations.py | 2 +- 78 files changed, 77 insertions(+), 77 deletions(-) rename src/datapack/modes/{common.py => emit.py} (100%) diff --git a/src/datapack/modes/_coupdetat/main.py b/src/datapack/modes/_coupdetat/main.py index d9e4a3741..c288caba4 100644 --- a/src/datapack/modes/_coupdetat/main.py +++ b/src/datapack/modes/_coupdetat/main.py @@ -3,7 +3,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/beat_the_kings/main.py b/src/datapack/modes/beat_the_kings/main.py index 8bf0a1db6..c0cecb400 100644 --- a/src/datapack/modes/beat_the_kings/main.py +++ b/src/datapack/modes/beat_the_kings/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/block_party/main.py b/src/datapack/modes/block_party/main.py index 731120ef0..494b94b49 100644 --- a/src/datapack/modes/block_party/main.py +++ b/src/datapack/modes/block_party/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/block_party/translations.py b/src/datapack/modes/block_party/translations.py index 251e13f82..748e47d63 100644 --- a/src/datapack/modes/block_party/translations.py +++ b/src/datapack/modes/block_party/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/boat_race/main.py b/src/datapack/modes/boat_race/main.py index 1e75e18c0..ea8497efc 100644 --- a/src/datapack/modes/boat_race/main.py +++ b/src/datapack/modes/boat_race/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/boat_race/translations.py b/src/datapack/modes/boat_race/translations.py index b2b60b2af..c4cae1008 100644 --- a/src/datapack/modes/boat_race/translations.py +++ b/src/datapack/modes/boat_race/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/bombardement/main.py b/src/datapack/modes/bombardement/main.py index f87793139..b1aaaa515 100644 --- a/src/datapack/modes/bombardement/main.py +++ b/src/datapack/modes/bombardement/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/bombardement/translations.py b/src/datapack/modes/bombardement/translations.py index 69ef87623..401aceb50 100644 --- a/src/datapack/modes/bombardement/translations.py +++ b/src/datapack/modes/bombardement/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/build_battle/main.py b/src/datapack/modes/build_battle/main.py index fe9ea0507..8acf22b58 100644 --- a/src/datapack/modes/build_battle/main.py +++ b/src/datapack/modes/build_battle/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/build_battle/resources.py b/src/datapack/modes/build_battle/resources.py index fb501d93c..3f5dd51b4 100644 --- a/src/datapack/modes/build_battle/resources.py +++ b/src/datapack/modes/build_battle/resources.py @@ -4,7 +4,7 @@ from beet.contrib.worldgen import Dimension from stewbeet.core import Mem, set_json_encoder -from ..common import register_structures +from ..emit import register_structures def write_resources() -> None: diff --git a/src/datapack/modes/capture_the_flag/main.py b/src/datapack/modes/capture_the_flag/main.py index dbeb1bb55..a7254487d 100644 --- a/src/datapack/modes/capture_the_flag/main.py +++ b/src/datapack/modes/capture_the_flag/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .kits import classes from .translations import write_translations diff --git a/src/datapack/modes/castagne/main.py b/src/datapack/modes/castagne/main.py index 33dc6efb9..ca560980e 100644 --- a/src/datapack/modes/castagne/main.py +++ b/src/datapack/modes/castagne/main.py @@ -4,7 +4,7 @@ from ...kits import Kit, KitItem from .._common.main import pvp_arena_kit -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/cigogne/main.py b/src/datapack/modes/cigogne/main.py index c3090a30a..4128fa9e3 100644 --- a/src/datapack/modes/cigogne/main.py +++ b/src/datapack/modes/cigogne/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/coin_chaser/main.py b/src/datapack/modes/coin_chaser/main.py index 0a3d6add4..a23e8d6d6 100644 --- a/src/datapack/modes/coin_chaser/main.py +++ b/src/datapack/modes/coin_chaser/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_no_drop, write_time_xp_bar +from ..emit import write_modes_calls, write_no_drop, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/coin_chaser/translations.py b/src/datapack/modes/coin_chaser/translations.py index 17a3700df..ab942e9ac 100644 --- a/src/datapack/modes/coin_chaser/translations.py +++ b/src/datapack/modes/coin_chaser/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/creeper_apocalypse/main.py b/src/datapack/modes/creeper_apocalypse/main.py index 0475d9a79..381d17d66 100644 --- a/src/datapack/modes/creeper_apocalypse/main.py +++ b/src/datapack/modes/creeper_apocalypse/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/creeper_apocalypse/translations.py b/src/datapack/modes/creeper_apocalypse/translations.py index 08f03e935..da25f1a1e 100644 --- a/src/datapack/modes/creeper_apocalypse/translations.py +++ b/src/datapack/modes/creeper_apocalypse/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/de_a_coudre/main.py b/src/datapack/modes/de_a_coudre/main.py index c8d93d8b8..201808098 100644 --- a/src/datapack/modes/de_a_coudre/main.py +++ b/src/datapack/modes/de_a_coudre/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/common.py b/src/datapack/modes/emit.py similarity index 100% rename from src/datapack/modes/common.py rename to src/datapack/modes/emit.py diff --git a/src/datapack/modes/feed_fast/main.py b/src/datapack/modes/feed_fast/main.py index c135a2413..85d7d01bc 100644 --- a/src/datapack/modes/feed_fast/main.py +++ b/src/datapack/modes/feed_fast/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/feed_fast/translations.py b/src/datapack/modes/feed_fast/translations.py index e3fac2a4c..d004725eb 100644 --- a/src/datapack/modes/feed_fast/translations.py +++ b/src/datapack/modes/feed_fast/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/fireblast/main.py b/src/datapack/modes/fireblast/main.py index 2468d58ed..de929df69 100644 --- a/src/datapack/modes/fireblast/main.py +++ b/src/datapack/modes/fireblast/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/fireblast/translations.py b/src/datapack/modes/fireblast/translations.py index b88b012c6..265d4f8d9 100644 --- a/src/datapack/modes/fireblast/translations.py +++ b/src/datapack/modes/fireblast/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/fish_fight/main.py b/src/datapack/modes/fish_fight/main.py index b03b9fa01..1a1420c19 100644 --- a/src/datapack/modes/fish_fight/main.py +++ b/src/datapack/modes/fish_fight/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_no_drop +from ..emit import write_modes_calls, write_no_drop from .translations import write_translations diff --git a/src/datapack/modes/gay_shooter/main.py b/src/datapack/modes/gay_shooter/main.py index ce7cd5682..06cf5ff3b 100644 --- a/src/datapack/modes/gay_shooter/main.py +++ b/src/datapack/modes/gay_shooter/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/gay_shooter/translations.py b/src/datapack/modes/gay_shooter/translations.py index cebe87faa..393cecaa7 100644 --- a/src/datapack/modes/gay_shooter/translations.py +++ b/src/datapack/modes/gay_shooter/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/glassrunner/main.py b/src/datapack/modes/glassrunner/main.py index 699aa53b3..01a7e6736 100644 --- a/src/datapack/modes/glassrunner/main.py +++ b/src/datapack/modes/glassrunner/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/glassrunner/resources.py b/src/datapack/modes/glassrunner/resources.py index 050db92d4..3a373b169 100644 --- a/src/datapack/modes/glassrunner/resources.py +++ b/src/datapack/modes/glassrunner/resources.py @@ -5,7 +5,7 @@ from beet import BlockTag, ItemModifier, LootTable, Predicate from stewbeet.core import Mem, set_json_encoder -from ..common import register_structures +from ..emit import register_structures def holding_predicate(flag: str) -> dict[str, Any]: diff --git a/src/datapack/modes/glassrunner/translations.py b/src/datapack/modes/glassrunner/translations.py index 80978104a..881660e59 100644 --- a/src/datapack/modes/glassrunner/translations.py +++ b/src/datapack/modes/glassrunner/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/infected/main.py b/src/datapack/modes/infected/main.py index ebb16400a..4084f0cbb 100644 --- a/src/datapack/modes/infected/main.py +++ b/src/datapack/modes/infected/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/laser_game/main.py b/src/datapack/modes/laser_game/main.py index 656d2576f..8f2069f91 100644 --- a/src/datapack/modes/laser_game/main.py +++ b/src/datapack/modes/laser_game/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/laser_game/translations.py b/src/datapack/modes/laser_game/translations.py index 0d169ffff..584853bc9 100644 --- a/src/datapack/modes/laser_game/translations.py +++ b/src/datapack/modes/laser_game/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/layers_2_teams/main.py b/src/datapack/modes/layers_2_teams/main.py index 721ef1485..e5d87fe22 100644 --- a/src/datapack/modes/layers_2_teams/main.py +++ b/src/datapack/modes/layers_2_teams/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/layers_4_teams/main.py b/src/datapack/modes/layers_4_teams/main.py index 02a6ad88b..149129ab5 100644 --- a/src/datapack/modes/layers_4_teams/main.py +++ b/src/datapack/modes/layers_4_teams/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/memory_mine/main.py b/src/datapack/modes/memory_mine/main.py index a8238ee81..849c0437a 100644 --- a/src/datapack/modes/memory_mine/main.py +++ b/src/datapack/modes/memory_mine/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_no_drop +from ..emit import write_modes_calls, write_no_drop from .translations import write_translations diff --git a/src/datapack/modes/memory_mine/translations.py b/src/datapack/modes/memory_mine/translations.py index 777293ca2..0b27f4fed 100644 --- a/src/datapack/modes/memory_mine/translations.py +++ b/src/datapack/modes/memory_mine/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/minigolf/main.py b/src/datapack/modes/minigolf/main.py index f95f5e70d..31887b1ea 100644 --- a/src/datapack/modes/minigolf/main.py +++ b/src/datapack/modes/minigolf/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/minigolf/translations.py b/src/datapack/modes/minigolf/translations.py index ccda3257a..c14cdb33e 100644 --- a/src/datapack/modes/minigolf/translations.py +++ b/src/datapack/modes/minigolf/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/mlg_a_coudre/main.py b/src/datapack/modes/mlg_a_coudre/main.py index 3571ede13..0a46ba0a6 100644 --- a/src/datapack/modes/mlg_a_coudre/main.py +++ b/src/datapack/modes/mlg_a_coudre/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/mlg_a_coudre/translations.py b/src/datapack/modes/mlg_a_coudre/translations.py index 3cfa60ef2..94137009b 100644 --- a/src/datapack/modes/mlg_a_coudre/translations.py +++ b/src/datapack/modes/mlg_a_coudre/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/moutron/main.py b/src/datapack/modes/moutron/main.py index e17bec6a6..eeb188fd2 100644 --- a/src/datapack/modes/moutron/main.py +++ b/src/datapack/modes/moutron/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/moutron/translations.py b/src/datapack/modes/moutron/translations.py index 5c8b19598..14a36e50a 100644 --- a/src/datapack/modes/moutron/translations.py +++ b/src/datapack/modes/moutron/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/murder_mystery/main.py b/src/datapack/modes/murder_mystery/main.py index 1cf39f4ba..ac595c4d5 100644 --- a/src/datapack/modes/murder_mystery/main.py +++ b/src/datapack/modes/murder_mystery/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_no_drop +from ..emit import write_modes_calls, write_no_drop from .translations import write_translations diff --git a/src/datapack/modes/murder_mystery/translations.py b/src/datapack/modes/murder_mystery/translations.py index 5b54d09fd..0c51dc183 100644 --- a/src/datapack/modes/murder_mystery/translations.py +++ b/src/datapack/modes/murder_mystery/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/one_shot/main.py b/src/datapack/modes/one_shot/main.py index 0986a71ff..e1830161f 100644 --- a/src/datapack/modes/one_shot/main.py +++ b/src/datapack/modes/one_shot/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_no_drop, write_time_xp_bar +from ..emit import write_modes_calls, write_no_drop, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/panic_chase/main.py b/src/datapack/modes/panic_chase/main.py index 358710bcf..1106ec82b 100644 --- a/src/datapack/modes/panic_chase/main.py +++ b/src/datapack/modes/panic_chase/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_no_drop +from ..emit import write_modes_calls, write_no_drop from .translations import write_translations diff --git a/src/datapack/modes/pillars_of_fortune/main.py b/src/datapack/modes/pillars_of_fortune/main.py index 774cf0f0d..26810d24a 100644 --- a/src/datapack/modes/pillars_of_fortune/main.py +++ b/src/datapack/modes/pillars_of_fortune/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/pitch_creep/main.py b/src/datapack/modes/pitch_creep/main.py index 6e6874d51..1c988f99d 100644 --- a/src/datapack/modes/pitch_creep/main.py +++ b/src/datapack/modes/pitch_creep/main.py @@ -3,7 +3,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem, Variants -from ..common import skinned_weapons, write_modes_calls, write_time_xp_bar +from ..emit import skinned_weapons, write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/pitch_creep/translations.py b/src/datapack/modes/pitch_creep/translations.py index 37d7469fb..4c7d21019 100644 --- a/src/datapack/modes/pitch_creep/translations.py +++ b/src/datapack/modes/pitch_creep/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/pitchout/main.py b/src/datapack/modes/pitchout/main.py index a8808de0c..2299fa3f3 100644 --- a/src/datapack/modes/pitchout/main.py +++ b/src/datapack/modes/pitchout/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem, ScoreCount, Variants -from ..common import skinned_weapons, write_modes_calls, write_no_drop +from ..emit import skinned_weapons, write_modes_calls, write_no_drop from .translations import write_translations diff --git a/src/datapack/modes/pitchout/translations.py b/src/datapack/modes/pitchout/translations.py index c356bfdd8..9fbb96d1f 100644 --- a/src/datapack/modes/pitchout/translations.py +++ b/src/datapack/modes/pitchout/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/protect_the_king/main.py b/src/datapack/modes/protect_the_king/main.py index 4eb6b093f..cdec0a8fa 100644 --- a/src/datapack/modes/protect_the_king/main.py +++ b/src/datapack/modes/protect_the_king/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/pvpswap/main.py b/src/datapack/modes/pvpswap/main.py index 16459eac4..a58d3c695 100644 --- a/src/datapack/modes/pvpswap/main.py +++ b/src/datapack/modes/pvpswap/main.py @@ -4,7 +4,7 @@ from ...kits import Kit from .._common.main import pvp_arena_kit -from ..common import write_modes_calls +from ..emit import write_modes_calls def write_mode(): diff --git a/src/datapack/modes/race/main.py b/src/datapack/modes/race/main.py index 6d2ecd1b6..70d691f28 100644 --- a/src/datapack/modes/race/main.py +++ b/src/datapack/modes/race/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/replicate_the_build/main.py b/src/datapack/modes/replicate_the_build/main.py index bb15a2080..ed610dd82 100644 --- a/src/datapack/modes/replicate_the_build/main.py +++ b/src/datapack/modes/replicate_the_build/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/replicate_the_build/resources.py b/src/datapack/modes/replicate_the_build/resources.py index 939ac60e7..4abbc8fbc 100644 --- a/src/datapack/modes/replicate_the_build/resources.py +++ b/src/datapack/modes/replicate_the_build/resources.py @@ -1,6 +1,6 @@ # Imports -from ..common import register_structures +from ..emit import register_structures def write_resources() -> None: diff --git a/src/datapack/modes/rush_the_flag/main.py b/src/datapack/modes/rush_the_flag/main.py index 0bf4f3fcb..f3a4e5dbc 100644 --- a/src/datapack/modes/rush_the_flag/main.py +++ b/src/datapack/modes/rush_the_flag/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/rush_the_flag/resources.py b/src/datapack/modes/rush_the_flag/resources.py index 39426f2bb..2cced130c 100644 --- a/src/datapack/modes/rush_the_flag/resources.py +++ b/src/datapack/modes/rush_the_flag/resources.py @@ -1,6 +1,6 @@ # Imports -from ..common import register_break_obsidian_advancement +from ..emit import register_break_obsidian_advancement def write_resources() -> None: diff --git a/src/datapack/modes/rush_the_point/main.py b/src/datapack/modes/rush_the_point/main.py index 562c2aa70..8e3ed4cc4 100644 --- a/src/datapack/modes/rush_the_point/main.py +++ b/src/datapack/modes/rush_the_point/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .kits import build_kits from .translations import write_translations diff --git a/src/datapack/modes/rush_the_point/resources.py b/src/datapack/modes/rush_the_point/resources.py index 2cdda88d6..1ea902b7e 100644 --- a/src/datapack/modes/rush_the_point/resources.py +++ b/src/datapack/modes/rush_the_point/resources.py @@ -3,7 +3,7 @@ from beet import Advancement, BlockTag from stewbeet.core import Mem, set_json_encoder -from ..common import register_break_obsidian_advancement +from ..emit import register_break_obsidian_advancement def write_resources() -> None: diff --git a/src/datapack/modes/sheepwars/main.py b/src/datapack/modes/sheepwars/main.py index 215d36d0c..aa0949f57 100644 --- a/src/datapack/modes/sheepwars/main.py +++ b/src/datapack/modes/sheepwars/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem, ScoreCount -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/shoot_da_sheep/main.py b/src/datapack/modes/shoot_da_sheep/main.py index 061a6d0f5..dd3192b22 100644 --- a/src/datapack/modes/shoot_da_sheep/main.py +++ b/src/datapack/modes/shoot_da_sheep/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/shoot_da_sheep/translations.py b/src/datapack/modes/shoot_da_sheep/translations.py index 968b2d4df..90a86c7b6 100644 --- a/src/datapack/modes/shoot_da_sheep/translations.py +++ b/src/datapack/modes/shoot_da_sheep/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/simultaneous_jump/main.py b/src/datapack/modes/simultaneous_jump/main.py index a33269212..353466877 100644 --- a/src/datapack/modes/simultaneous_jump/main.py +++ b/src/datapack/modes/simultaneous_jump/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/simultaneous_jump/translations.py b/src/datapack/modes/simultaneous_jump/translations.py index 33b0f2c43..fa5bc3443 100644 --- a/src/datapack/modes/simultaneous_jump/translations.py +++ b/src/datapack/modes/simultaneous_jump/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/snowball_painter/main.py b/src/datapack/modes/snowball_painter/main.py index 348e00623..c5c5ecbf3 100644 --- a/src/datapack/modes/snowball_painter/main.py +++ b/src/datapack/modes/snowball_painter/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/snowball_painter/translations.py b/src/datapack/modes/snowball_painter/translations.py index 706c17a35..e047e451e 100644 --- a/src/datapack/modes/snowball_painter/translations.py +++ b/src/datapack/modes/snowball_painter/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/spectres_game/main.py b/src/datapack/modes/spectres_game/main.py index b9af26e25..887012c01 100644 --- a/src/datapack/modes/spectres_game/main.py +++ b/src/datapack/modes/spectres_game/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .kits import spectre_kit, visible_kit from .translations import write_translations diff --git a/src/datapack/modes/spleef/main.py b/src/datapack/modes/spleef/main.py index b670a7442..9087e8ad8 100644 --- a/src/datapack/modes/spleef/main.py +++ b/src/datapack/modes/spleef/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem, ScoreCount -from ..common import write_modes_calls +from ..emit import write_modes_calls from .translations import write_translations diff --git a/src/datapack/modes/spleef/translations.py b/src/datapack/modes/spleef/translations.py index df7d69d40..c6096f1b9 100644 --- a/src/datapack/modes/spleef/translations.py +++ b/src/datapack/modes/spleef/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/thunder_spear/main.py b/src/datapack/modes/thunder_spear/main.py index e0fa828fb..2bfff7462 100644 --- a/src/datapack/modes/thunder_spear/main.py +++ b/src/datapack/modes/thunder_spear/main.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/thunder_spear/translations.py b/src/datapack/modes/thunder_spear/translations.py index 222618ebf..9fd95959b 100644 --- a/src/datapack/modes/thunder_spear/translations.py +++ b/src/datapack/modes/thunder_spear/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/tnt_run/main.py b/src/datapack/modes/tnt_run/main.py index 14e28e20e..f475ba275 100644 --- a/src/datapack/modes/tnt_run/main.py +++ b/src/datapack/modes/tnt_run/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/tnt_run/translations.py b/src/datapack/modes/tnt_run/translations.py index 54616e0ea..9760c0fc0 100644 --- a/src/datapack/modes/tnt_run/translations.py +++ b/src/datapack/modes/tnt_run/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): diff --git a/src/datapack/modes/traitors_game/main.py b/src/datapack/modes/traitors_game/main.py index c50183fdd..eb79079f5 100644 --- a/src/datapack/modes/traitors_game/main.py +++ b/src/datapack/modes/traitors_game/main.py @@ -4,7 +4,7 @@ from stewbeet import Mem, write_function from ...kits import Kit, KitItem, Variants -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/warden_escape/main.py b/src/datapack/modes/warden_escape/main.py index 5f504e98c..73a017e78 100644 --- a/src/datapack/modes/warden_escape/main.py +++ b/src/datapack/modes/warden_escape/main.py @@ -2,7 +2,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations diff --git a/src/datapack/modes/warden_escape/resources.py b/src/datapack/modes/warden_escape/resources.py index 1cc341497..b945afddd 100644 --- a/src/datapack/modes/warden_escape/resources.py +++ b/src/datapack/modes/warden_escape/resources.py @@ -3,7 +3,7 @@ from beet import Advancement from stewbeet.core import Mem, set_json_encoder -from ..common import register_sounds +from ..emit import register_sounds def write_resources() -> None: diff --git a/src/datapack/modes/warden_escape/translations.py b/src/datapack/modes/warden_escape/translations.py index 7884ac292..e56b6bf1f 100644 --- a/src/datapack/modes/warden_escape/translations.py +++ b/src/datapack/modes/warden_escape/translations.py @@ -3,7 +3,7 @@ # Imports from stewbeet import Mem, write_function -from ..common import write_server_announce +from ..emit import write_server_announce def write_translations(): From 110a6f63a90800f0ad7b880a93fb9f0c7eb9114e Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:32:47 +0200 Subject: [PATCH 05/10] =?UTF-8?q?chore:=20=F0=9F=94=A7=20Diffs=20lisibles?= =?UTF-8?q?=20sur=20src,=20masqu=C3=A9s=20seulement=20sur=20les=20fichiers?= =?UTF-8?q?=20g=C3=A9n=C3=A9r=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index d47f08155..b36af79af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ -* -diff -merge +build/** -diff -merge +*.zip -diff -merge +*.nbt -diff -merge +*.ogg -diff -merge From 9f0a6fb4f70bde04762479786954f38dee18d82f Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:33:57 +0200 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20Inline?= =?UTF-8?q?=20des=20variables=20=C3=A0=20usage=20unique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/datapack/misc_links/starting_pop_ups.py | 9 +++------ src/datapack/survival_maps/utils.py | 7 +++---- src/validation.py | 6 ++---- tools/check_output_drift.py | 4 ++-- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/datapack/misc_links/starting_pop_ups.py b/src/datapack/misc_links/starting_pop_ups.py index ac9ae176e..05c421093 100644 --- a/src/datapack/misc_links/starting_pop_ups.py +++ b/src/datapack/misc_links/starting_pop_ups.py @@ -16,18 +16,15 @@ def main() -> None: # For each minigame, for mode in MODES: - mode_id: str = mode.id - mode_name: str = mode.display_name(lang) - # Prepare the title of the advancement - title: list[JsonDict] = [{"text":"'","color":"aqua"},{"text":mode_name,"color":"yellow"}] + title: list[JsonDict] = [{"text":"'","color":"aqua"},{"text":mode.display_name(lang),"color":"yellow"}] if lang == "fr": title.append({"text":"' sélectionné !"}) elif lang == "en": title.append({"text":"' selected!"}) # Write the advancement (called in data/switch/function/engine/signals/macro_start.mcfunction) - Mem.ctx.data[ns].advancements[f"pop_ups/{mode_id}_{lang}"] = set_json_encoder(Advancement({ + Mem.ctx.data[ns].advancements[f"pop_ups/{mode.id}_{lang}"] = set_json_encoder(Advancement({ "display": { "icon": {"id": "minecraft:stone","components": {"minecraft:item_model": f"{ns}:letter"}}, "title": title, @@ -46,7 +43,7 @@ def main() -> None: })) # Write the reward function that revoke the advancement - write_function(f"{ns}:engine/pop_ups/schedule", f"advancement revoke @a only {ns}:pop_ups/{mode_id}_{lang}") + write_function(f"{ns}:engine/pop_ups/schedule", f"advancement revoke @a only {ns}:pop_ups/{mode.id}_{lang}") # Write the function that revokes the advancement write_function(f"{ns}:engine/pop_ups/revoke", f"schedule function {ns}:engine/pop_ups/schedule 1s") diff --git a/src/datapack/survival_maps/utils.py b/src/datapack/survival_maps/utils.py index 2dfc612bf..e66c852fb 100644 --- a/src/datapack/survival_maps/utils.py +++ b/src/datapack/survival_maps/utils.py @@ -791,9 +791,8 @@ def generate_map_usage_file() -> None: # Get for each modes the maps they use modes_usage: dict[str, list[str]] = {} for mode in MODES: - mode_id: str = mode.id - start_file: str = f"{ns}:modes/{mode_id}/" - modes_usage[mode_id] = [] + start_file: str = f"{ns}:modes/{mode.id}/" + modes_usage[mode.id] = [] # Get the start function of the mode and search for "function switch:utils/choose_map_for {id:"block_party", maps:["pitch_creep_1","octogone_nether_ice"]}" function_content: str = Mem.ctx.data.functions.get(start_file + "start_common", Function()).text.strip() @@ -805,7 +804,7 @@ def generate_map_usage_file() -> None: splitted: list[str] = function_content.split(CHOOSE_MAP_FOR) if len(splitted) > 1: maps_str: str = splitted[1].split("\n")[0].split("maps:")[1].split("}")[0] - modes_usage[mode_id] = json.loads(maps_str) + modes_usage[mode.id] = json.loads(maps_str) # Now, generate the maps_usage dictionary maps_usage: dict[str, list[str]] = {} diff --git a/src/validation.py b/src/validation.py index 8ac9531d3..438b1fb6a 100644 --- a/src/validation.py +++ b/src/validation.py @@ -156,17 +156,15 @@ def check_maps() -> list[Problem]: problems: list[Problem] = [] for name, paths in sorted(Validation.referenced_maps().items()): if name not in known: - culprit: str = sorted(paths)[0] problems.append(Problem( - f"unknown map '{name}' referenced by {culprit}", + f"unknown map '{name}' referenced by {sorted(paths)[0]}", Validation.suggest(name, sorted(generated_maps)))) return problems @staticmethod def report_unvoted() -> None: """ Log the modes that generate but never reach the vote, and the groups nobody uses. """ - voted: set[str] = {mode.id for mode in MODES} - unvoted: list[str] = sorted(Validation.declared_folders() - voted) + unvoted: list[str] = sorted(Validation.declared_folders() - {mode.id for mode in MODES}) if unvoted: stp.info(f"{len(unvoted)} mode(s) generated but absent from the vote: {', '.join(unvoted)}") diff --git a/tools/check_output_drift.py b/tools/check_output_drift.py index 6efe50d2b..434046f99 100644 --- a/tools/check_output_drift.py +++ b/tools/check_output_drift.py @@ -42,8 +42,8 @@ def drifted_paths() -> list[str]: Returns: list[str]: Porcelain paths, ex: ["build/datapack/data/switch/function/tick.mcfunction"] """ - status: subprocess.CompletedProcess[str] = OutputDrift.run("git", "status", "--porcelain", "--", OUTPUT_FOLDER) - return [line[3:].strip().strip('"') for line in status.stdout.splitlines() if line.strip()] + status: str = OutputDrift.run("git", "status", "--porcelain", "--", OUTPUT_FOLDER).stdout + return [line[3:].strip().strip('"') for line in status.splitlines() if line.strip()] @staticmethod def build() -> bool: From a681c1639c643683fd6255f5827f0e5ffac4dd6b Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:36:13 +0200 Subject: [PATCH 07/10] =?UTF-8?q?refactor(shop):=20=E2=99=BB=EF=B8=8F=20Le?= =?UTF-8?q?s=20modes=20d=C3=A9clarent=20leur=20boutique,=20le=20sous-syst?= =?UTF-8?q?=C3=A8me=20consomme=20le=20registre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/datapack/modes/__init__.py | 11 +++++++++ src/datapack/modes/infected/shop.py | 3 ++- src/datapack/modes/pitchout/shop.py | 3 ++- src/datapack/modes/sheepwars/shop.py | 3 ++- src/datapack/modes/spectres_game/shop.py | 3 ++- src/datapack/modes/spleef/shop.py | 3 ++- src/datapack/shop/main.py | 4 ++-- src/datapack/shop/shared_memory.py | 29 ++++++++++++++---------- src/datapack/shop/utils.py | 6 ++--- src/datapack/survival_maps/utils.py | 2 +- 10 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/datapack/modes/__init__.py b/src/datapack/modes/__init__.py index a9bd22214..e5b492fd5 100644 --- a/src/datapack/modes/__init__.py +++ b/src/datapack/modes/__init__.py @@ -3,8 +3,14 @@ import importlib from pathlib import Path +from stouputils.typing import JsonDict + from .definitions import write_modes_load_file +# Constants +MODE_SHOPS: dict[str, dict[str, JsonDict]] = {} +""" Shop of every mode declaring one, keyed by mode id and filled by generate_all_modes(). """ + # Main function def generate_all_modes(): @@ -29,3 +35,8 @@ def generate_all_modes(): resources = importlib.import_module(f".{entry.name}.resources", package=__package__) if hasattr(resources, "write_resources"): resources.write_resources() + + # A mode may sell upgrades, declared as SHOP in a sibling shop.py + if (entry / "shop.py").is_file(): + shop = importlib.import_module(f".{entry.name}.shop", package=__package__) + MODE_SHOPS[entry.name] = shop.SHOP diff --git a/src/datapack/modes/infected/shop.py b/src/datapack/modes/infected/shop.py index 29f0d1be8..476942598 100644 --- a/src/datapack/modes/infected/shop.py +++ b/src/datapack/modes/infected/shop.py @@ -2,7 +2,7 @@ # Infected shop from stouputils import JsonDict -INFECTED: JsonDict = { +SHOP: JsonDict = { "sword": { "ok_messages": { "fr": "Votre achat pour améliorer votre épée a été effectué !", @@ -129,3 +129,4 @@ }, } +""" Upgrades sold by this mode's shop, collected by the modes registry. """ diff --git a/src/datapack/modes/pitchout/shop.py b/src/datapack/modes/pitchout/shop.py index 7f946ddb0..7d38218ff 100644 --- a/src/datapack/modes/pitchout/shop.py +++ b/src/datapack/modes/pitchout/shop.py @@ -2,7 +2,7 @@ # Pitchout shop from stouputils import JsonDict -PITCHOUT: JsonDict = { +SHOP: JsonDict = { "boots": { "ok_messages": { "fr": "Votre achat pour les bottes en cuir a été effectué !", @@ -46,3 +46,4 @@ }, } +""" Upgrades sold by this mode's shop, collected by the modes registry. """ diff --git a/src/datapack/modes/sheepwars/shop.py b/src/datapack/modes/sheepwars/shop.py index c2add867c..955ee1bfa 100644 --- a/src/datapack/modes/sheepwars/shop.py +++ b/src/datapack/modes/sheepwars/shop.py @@ -2,7 +2,7 @@ # Sheepwars shop from stouputils import JsonDict -SHEEPWARS: JsonDict = { +SHOP: JsonDict = { "chosen_kit": {}, "kit_health": { "ok_messages": { @@ -153,3 +153,4 @@ }, } +""" Upgrades sold by this mode's shop, collected by the modes registry. """ diff --git a/src/datapack/modes/spectres_game/shop.py b/src/datapack/modes/spectres_game/shop.py index 5421ef785..a1deb7dd6 100644 --- a/src/datapack/modes/spectres_game/shop.py +++ b/src/datapack/modes/spectres_game/shop.py @@ -9,7 +9,7 @@ {"price": 150, "hover_text": {"fr": "5 -> 6 TNT", "en": "5 -> 6 TNT"}}, ] -SPECTRES_GAME: JsonDict = { +SHOP: JsonDict = { "sp_arrows": { "ok_messages": { "fr": "Votre achat pour augmenter le nombre de fleches pour votre classe Spectre a été effectué !", @@ -95,3 +95,4 @@ }, } +""" Upgrades sold by this mode's shop, collected by the modes registry. """ diff --git a/src/datapack/modes/spleef/shop.py b/src/datapack/modes/spleef/shop.py index 2c0408df0..4af09ffe8 100644 --- a/src/datapack/modes/spleef/shop.py +++ b/src/datapack/modes/spleef/shop.py @@ -2,7 +2,7 @@ # Spleef shop from stouputils import JsonDict -SPLEEF: JsonDict = { +SHOP: JsonDict = { "snow_block": { "ok_messages": { "fr": "Votre achat pour augmenter votre nombre de blocs de neige a été effectué !", @@ -49,3 +49,4 @@ }, } +""" Upgrades sold by this mode's shop, collected by the modes registry. """ diff --git a/src/datapack/shop/main.py b/src/datapack/shop/main.py index aed28e795..62c90f616 100644 --- a/src/datapack/shop/main.py +++ b/src/datapack/shop/main.py @@ -1,6 +1,6 @@ # Imports -from .shared_memory import SHOPS +from .shared_memory import ordered_shops from .utils import ( general_translations, generate_shop, @@ -18,7 +18,7 @@ def main() -> None: write_raw_functions() # Generate all the shops - for i, (shop_name, shop_dict) in enumerate(SHOPS.items()): + for i, (shop_name, shop_dict) in enumerate(ordered_shops().items()): generate_shop(i, shop_name, shop_dict) # Write the trigger function diff --git a/src/datapack/shop/shared_memory.py b/src/datapack/shop/shared_memory.py index 5e210c8f2..22bf54b1f 100644 --- a/src/datapack/shop/shared_memory.py +++ b/src/datapack/shop/shared_memory.py @@ -2,11 +2,7 @@ # Imports from stewbeet import JsonDict, Mem -from ..modes.infected.shop import INFECTED -from ..modes.pitchout.shop import PITCHOUT -from ..modes.sheepwars.shop import SHEEPWARS -from ..modes.spectres_game.shop import SPECTRES_GAME -from ..modes.spleef.shop import SPLEEF +from ..modes import MODE_SHOPS # Constants STAR: str = "✮" @@ -27,13 +23,22 @@ def get_money() -> dict[str, JsonDict]: "en": "Select a kit by clicking on the name!" } SHEEPWARS_KIT_OFFSET: int = 50 -SHOPS: dict[str, dict[str, JsonDict]] = { - "pitchout": PITCHOUT, - "infected": INFECTED, - "spleef": SPLEEF, - "sheepwars": SHEEPWARS, - "spectres_game": SPECTRES_GAME, -} +SHOP_ORDER: tuple[str, ...] = ("pitchout", "infected", "spleef", "sheepwars", "spectres_game") +""" Frozen display order of the minigame shops. + +A shop's rank drives its trigger score range (see get_shop_range), which is what a player's click +sends, so reordering this changes the generated commands. A shop missing here is appended last. +""" + +def ordered_shops() -> dict[str, dict[str, JsonDict]]: + """ Every mode shop, in display order. + + Returns: + dict[str, dict[str, JsonDict]]: Mode id -> its upgrades + """ + listed: list[str] = [name for name in SHOP_ORDER if name in MODE_SHOPS] + return {name: MODE_SHOPS[name] for name in listed + [name for name in MODE_SHOPS if name not in SHOP_ORDER]} + # Function paths, relative to the project namespace (e.g. f"{ns}:{LOAD_PATH}") LOAD_PATH: str = "shop/_load" TRIGGER_PATH: str = "shop/trigger" diff --git a/src/datapack/shop/utils.py b/src/datapack/shop/utils.py index 31a59aa8b..bd4e5693e 100644 --- a/src/datapack/shop/utils.py +++ b/src/datapack/shop/utils.py @@ -13,12 +13,12 @@ REFUND_PERCENTAGE, SHEEPWARS_CHOOSE_KIT, SHEEPWARS_KIT_OFFSET, - SHOPS, STAR, TRIGGER_PATH, USERNAME_CHANGE_PATH, get_money, get_shop_range, + ordered_shops, ) @@ -309,7 +309,7 @@ def generate_trigger() -> None: # Minigames shops """) - for i, (shop_name, _) in enumerate(SHOPS.items()): + for i, (shop_name, _) in enumerate(ordered_shops().items()): mini, maxi = get_shop_range(i) write_function(f"{ns}:{TRIGGER_PATH}", f"execute if score @s {ns}.trigger.shop matches {mini}..{maxi} run function {ns}:shop/{shop_name}") @@ -340,7 +340,7 @@ def general_translations() -> None: tellraw {selector} [{{"text":"[","color":"#1b1796"}},{{"text":"{label.replace('X', 'Switch')}","color":"blue"}},{{"text":"]","color":"#1b1796"}},{{"text":" - ","color":"blue"}},{{"score":{{"name":"@s","objective":"{ns}.money"}},"color":"blue","underlined":true}},{json.dumps(get_money()[lang_id])},{{"text":"\\n"}}] """) # For each shop, write the access text - for i, shop_name in enumerate(SHOPS.keys()): + for i, shop_name in enumerate(ordered_shops()): mini: int = get_shop_range(i)[0] titled: str = shop_name.replace("_", " ").title() write_function(path, f"""tellraw {selector} [{{"text":"➤ ","color":"#1b1796","click_event":{{"action":"run_command","command":"/trigger {ns}.trigger.shop set {mini}"}}, "hover_event":{{"action":"show_text","value":{{"text":"{access_text.replace('X', titled)}","color":"gray"}}}}}},{{"text":"[","color":"#1b1796"}},{{"text":"{titled}","color":"blue"}},{{"text":"]","color":"#1b1796"}}]""") diff --git a/src/datapack/survival_maps/utils.py b/src/datapack/survival_maps/utils.py index e66c852fb..a2c4e018d 100644 --- a/src/datapack/survival_maps/utils.py +++ b/src/datapack/survival_maps/utils.py @@ -8,6 +8,7 @@ from beet import Function from stewbeet.core import Mem, write_function +from ..modes.definitions import MODES from .shared_memory import SharedMemory, generated_maps, survival_maps @@ -784,7 +785,6 @@ def generate_map_usage_file() -> None: config (dict): The configuration of the project """ ns: str = Mem.ctx.project_id - from ..modes.definitions import MODES path: str = f"{Mem.ctx.directory}/map_usage.json" CHOOSE_MAP_FOR: str = f"function {ns}:utils/choose_map_for" From 95744af67c78108b43dc3feb22e169f94613e9a0 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:38:04 +0200 Subject: [PATCH 08/10] =?UTF-8?q?feat(tools):=20=E2=9C=A8=20Scaffolding=20?= =?UTF-8?q?d'un=20nouveau=20mode=20de=20jeu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/new_mode.py | 213 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 tools/new_mode.py diff --git a/tools/new_mode.py b/tools/new_mode.py new file mode 100644 index 000000000..182263042 --- /dev/null +++ b/tools/new_mode.py @@ -0,0 +1,213 @@ +""" Scaffold a new minigame: creates its package and prints the MODES entry to paste. + +The generated mode builds and is playable right away through its _force_start, so a contributor +can see something in game before reading anything about the pipeline. +""" +# Imports +import re +import sys +from pathlib import Path + +import stouputils as stp + +# Constants +MODES_FOLDER: Path = Path(__file__).resolve().parent.parent / "src" / "datapack" / "modes" +""" Folder receiving one package per minigame. """ + +VALID_ID: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9_]*$") +""" A mode id is a lowercase python identifier: it is both a folder name and a storage key. """ + +MAIN_TEMPLATE: str = ''' +# Imports +from stewbeet import Mem, write_function + +from ..emit import write_modes_calls, write_time_xp_bar +from .translations import write_translations + +DURATION: int = 300 +""" How long a game lasts, in seconds. """ + + +def write_mode(): + ns: str = Mem.ctx.project_id + mode: str = "__MODE__" + path: str = f"{ns}:modes/{mode}" + + # Write /calls/ (the dispatch the engine goes through) and /translations/ + write_modes_calls(mode) + write_translations() + + # /death (called when a player dies: turn them into a spectator) + write_function(f"{path}/death", f""" +function {path}/translations/death +function {ns}:modes/_common/death_spectator +""") + + # /joined (a player joining a running game only gets to watch) + write_function(f"{path}/joined", f""" +gamemode spectator @s +function {path}/death +""") + + # /process_end (end the game once a single player is left standing) + write_function(f"{path}/process_end", f""" +function {ns}:modes/_common/process_end/last_survivor {{death:"{path}/death"}} +""") + + # /start (once, when the game begins: pick a map and reset the counters) + write_function(f"{path}/start", f""" +effect give @a[tag=!detached] saturation infinite 255 true + +# Pick the arena among the maps declared in survival_maps/definitions.py +scoreboard players set #do_spreadplayers {ns}.data 1 +function {ns}:utils/choose_map_for {{id:"{mode}", maps:["switch_space"]}} + +function {path}/translations/start + +scoreboard players set #__MODE___seconds {ns}.data -6 +scoreboard players set #process_end {ns}.data 0 +""") + + # /second (once per game second: countdown, then the time limit) + write_function(f"{path}/second", f""" +scoreboard players add #__MODE___seconds {ns}.data 1 +function {path}/xp_bar + +execute if score #__MODE___seconds {ns}.data matches -5..0 as @a[tag=!detached] at @s run playsound entity.experience_orb.pickup ambient @s +function {path}/translations/second + +execute if score #__MODE___seconds {ns}.data matches {DURATION}.. run function {path}/process_end +""") + + # /tick (every game tick: catch deaths and check whether the game is over) + write_function(f"{path}/tick", f""" +function {ns}:utils/on_death_run_function {{function:"{path}/death"}} + +scoreboard players set #remaining_players {ns}.data 0 +execute store result score #remaining_players {ns}.data if entity @a[tag=!detached,gamemode=!spectator] +execute if score #__MODE___seconds {ns}.data matches 1.. if score #remaining_players {ns}.data matches ..1 run function {path}/process_end +""") + + # /stop (clean up everything /start created) + write_function(f"{path}/stop", f""" +scoreboard players reset #__MODE___seconds {ns}.data +""") + + # /xp_bar (the XP bar doubles as the remaining time gauge) + write_time_xp_bar(f"{path}/xp_bar", DURATION, "#__MODE___seconds", "#__MODE___seconds") + +''' +""" Body of a new mode's main.py, with __MODE__ standing for its id. """ + +TRANSLATIONS_TEMPLATE: str = ''' +# Imports +from stewbeet import Mem, write_function + +from ..emit import write_server_announce + + +def write_translations(): + """ Write the __MODE__ translation functions at switch:modes/__MODE__/translations/* """ + ns: str = Mem.ctx.project_id + path: str = f"{ns}:modes/__MODE__/translations" + + # /death + write_function(f"{path}/death", f""" +# French +execute if entity @s[gamemode=!spectator] run tellraw @a[scores={{{ns}.lang=0}},tag=!detached] [{{"selector":"@s","color":"red"}},{{"text":" est mort !"}}] + +# English +execute if entity @s[gamemode=!spectator] run tellraw @a[scores={{{ns}.lang=1}},tag=!detached] [{{"selector":"@s","color":"red"}},{{"text":" died!"}}] +""") + + # /second (the 5 second countdown before the game starts) + write_function(f"{path}/second", "\\n".join( + f"""execute if score #__MODE___seconds {ns}.data matches {-second} run title @a[scores={{{ns}.lang={lang}}},tag=!detached] title {{"text":"{second}","color":"red"}}""" + for lang in (0, 1) for second in (5, 4, 3, 2, 1) + ) + "\\n" + "\\n".join( + f"""execute if score #__MODE___seconds {ns}.data matches 0 run title @a[scores={{{ns}.lang={lang}}},tag=!detached] title {{"text":"{go}","color":"red"}}""" + for lang, go in ((0, "GO !"), (1, "GO!")) + )) + + # /start + write_server_announce(f"{path}/start", "Lancement de la partie de __MODE__ !", "__MODE__ game is starting!") + +''' +""" Body of a new mode's translations.py, with __MODE__ standing for its id. """ + +SPEC_TEMPLATE: str = ''' ModeSpec( + id="__MODE__", name_fr="__NAME__", + min_players=2, max_players=UNLIMITED, + estimated_time="2-5 mins", inspiration="?", suggested_by="?", developed_by="?", + description=Localized( + fr=("Première ligne de description.", "Deuxième ligne."), + en=("First description line.", "Second line."), + ), + ),''' +""" MODES entry to paste in definitions.py, with __MODE__ and __NAME__ standing for the mode. """ + + +# Classes +class NewMode: + + @staticmethod + def display_name(mode: str) -> str: + """ Default human readable name built from a mode id. + + Args: + mode (str): The mode id, ex: "tnt_run" + Returns: + str: The name, ex: "Tnt Run" + + Examples: + >>> NewMode.display_name("tnt_run") + 'Tnt Run' + """ + return mode.replace("_", " ").title() + + @staticmethod + def create(mode: str) -> Path: + """ Write the package of a new mode. + + Args: + mode (str): The mode id, ex: "tnt_run" + Returns: + Path: The created folder + """ + folder: Path = MODES_FOLDER / mode + folder.mkdir() + (folder / "__init__.py").write_text("", encoding="utf-8") + (folder / "main.py").write_text(MAIN_TEMPLATE.replace("__MODE__", mode), encoding="utf-8") + (folder / "translations.py").write_text(TRANSLATIONS_TEMPLATE.replace("__MODE__", mode), encoding="utf-8") + return folder + + @staticmethod + def main() -> int: + """ Read the mode id from the command line, scaffold it, then print what is left to do. + + Returns: + int: Process exit code + """ + if len(sys.argv) != 2: + stp.error("usage: python tools/new_mode.py ") + return 1 + + mode: str = sys.argv[1] + if not VALID_ID.match(mode): + stp.error(f"'{mode}' is not a valid mode id: lowercase letters, digits and underscores only") + return 1 + if (MODES_FOLDER / mode).exists(): + stp.error(f"src/datapack/modes/{mode}/ already exists") + return 1 + + stp.info(f"Created {NewMode.create(mode)}") + stp.info("It already builds. Launch it in game with:") + stp.info(f" /function switch:test_mode\n /function switch:modes/{mode}/_force_start") + stp.info("To put it in the vote, paste this into MODES in src/datapack/modes/definitions.py:\n") + print(SPEC_TEMPLATE.replace("__MODE__", mode).replace("__NAME__", NewMode.display_name(mode))) + return 0 + + +if __name__ == "__main__": + sys.exit(NewMode.main()) + From 4496fb570dfd075ef42f2ea7dee6fd6a2d419ef5 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:42:54 +0200 Subject: [PATCH 09/10] =?UTF-8?q?feat(ci):=20=E2=9C=A8=20Conventions=20arc?= =?UTF-8?q?hitecturales=20v=C3=A9rifi=C3=A9es,=20config=20ruff=20dans=20le?= =?UTF-8?q?=20d=C3=A9p=C3=B4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/checks.yml | 30 ++ ruff.toml | 15 + src/datapack/misc_links/starting_pop_ups.py | 3 +- src/datapack/modes/catalogue.py | 490 ++++++++++++++++++++ src/datapack/modes/definitions.py | 490 +------------------- src/datapack/survival_maps/utils.py | 2 +- src/validation.py | 3 +- tools/check_conventions.py | 158 +++++++ 8 files changed, 703 insertions(+), 488 deletions(-) create mode 100644 .github/workflows/checks.yml create mode 100644 ruff.toml create mode 100644 src/datapack/modes/catalogue.py create mode 100644 tools/check_conventions.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 000000000..2a46077ed --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,30 @@ +name: Checks + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + static: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install dependencies + run: pip install --upgrade stewbeet ruff pyright + + - name: Lint + run: ruff check src tools + + - name: Type check + run: pyright + + - name: Architectural conventions + run: python tools/check_conventions.py diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..d91992e4e --- /dev/null +++ b/ruff.toml @@ -0,0 +1,15 @@ +exclude = [".git", ".ruff_cache", ".venv", ".vscode", "build", "dist", "*.pyi"] +line-length = 200 + +[lint] +select = ["E", "W", "F", "I", "N", "UP", "B", "C4", "T20", "RUF"] +ignore = [ + "T201", # print() is how the tools talk to the developer + "W191", # indentation is tabs, alignment is spaces + "N803", # argument names follow the datapack vocabulary + "N806", # local constants stay uppercase when they mirror a generated name +] + +[lint.isort] +# Keeps the "name as name" re-exports of a package grouped in a single statement +combine-as-imports = true diff --git a/src/datapack/misc_links/starting_pop_ups.py b/src/datapack/misc_links/starting_pop_ups.py index 05c421093..3c3d585b3 100644 --- a/src/datapack/misc_links/starting_pop_ups.py +++ b/src/datapack/misc_links/starting_pop_ups.py @@ -4,7 +4,8 @@ from stewbeet import Mem, set_json_encoder, write_function from stouputils.typing import JsonDict -from ..modes.definitions import LANG_PRETEXT, MODES +from ..modes.catalogue import MODES +from ..modes.definitions import LANG_PRETEXT # Main function diff --git a/src/datapack/modes/catalogue.py b/src/datapack/modes/catalogue.py new file mode 100644 index 000000000..e13bfc7c5 --- /dev/null +++ b/src/datapack/modes/catalogue.py @@ -0,0 +1,490 @@ +""" The minigames and vote groups of the server, as one declarative table. + +A mode generates even when its entry is commented out here: only its presence in the vote depends +on it. Kept as a single table on purpose, so one Ctrl+F finds any game. +""" +# Imports +from .spec import UNLIMITED, GroupSpec, Localized, ModeSpec + +# Constants +# Minigames +MODES: tuple[ModeSpec, ...] = ( + ModeSpec( + id="pitch_creep", name_fr="Pitch Creep", group="creepers", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Pitchout/Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez en repoussant les creepers", "dans une arène fermée."), + en=("Survive by pushing away creepers", "in a closed arena."), + ), + ), + ModeSpec( + id="laser_game", name_fr="Laser Game", + min_players=2, max_players=UNLIMITED, + estimated_time="2m30s", inspiration="The physical world", suggested_by="Vigo", developed_by="Stoupy", + description=Localized( + fr=( + "Affrontez l'équipe adverse grâce à vos fusils-laser et faites", + "le plus d'élimination possible pendant un temps imparti !", + ), + en=( + "Battle the enemy team with your laser rifles and get", + "as many takedowns as possible within the allotted time!", + ), + ), + ), + ModeSpec( + id="warden_escape", name_fr="Warden Escape", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Minecraft 1.19", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux wardens dans", "une arène fermée."), + en=("Survive the wardens in", "a closed arena."), + ), + ), + ModeSpec( + id="pitchout", name_fr="Pitchout", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Poussez vos adversaires dans l'eau", "et soyez le dernier survivant !"), + en=("Push your opponents into the water", "and be the last survivor!"), + ), + ), + ModeSpec( + id="creeper_apocalypse", name_fr="Creeper Apocalypse", group="creepers", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Expiration", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux creepers qui se", "multiplient et explosent."), + en=("Survive the creepers that", "duplicate and explode."), + ), + ), + ModeSpec( + id="traitors_game", name_fr="Traitors Game", + min_players=4, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="Taupe Gun UHC", suggested_by="Luxio", developed_by="Stoupy", + description=Localized( + fr=( + "Des traitres se cachent parmi les joueurs.", + "Innocents contre Traitres, quel camp va-t-il gagner ?", + ), + en=("Traitors are hinding among players.", "Innocents versus Traitors, which side will win?"), + ), + ), + # ModeSpec( + # id="boat_race", name_fr="Boat Race", + # min_players=1, max_players=UNLIMITED, + # estimated_time="3-8 mins", inspiration="Expiration", suggested_by="Stoupy", developed_by="LTHCTheMaster", + # description=Localized( + # fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), + # en=("Race for reaching the finish line", "in first to claim the win!"), + # ), + # ), + ModeSpec( + id="spectres_game", name_fr="Spectres Game", + min_players=2, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="Expiration", suggested_by="Luxio", developed_by="Stoupy", + description=Localized( + fr=( + "Deux camps : un avec que des joueurs visibles,", + "l'autre avec des spectres, quel camp va-t-il gagner ?", + ), + en=("Two sides: one with visible players,", "the other with spectres, which side will win?"), + ), + ), + ModeSpec( + id="race", name_fr="Race", + min_players=1, max_players=UNLIMITED, + estimated_time="3-6 mins", inspiration="Racing games", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), + en=("Race for reaching the finish line", "in first to claim the win!"), + ), + ), + ModeSpec( + id="rush_the_point", name_fr="Rush The Point", group="ctpf", + min_players=8, max_players=UNLIMITED, + estimated_time="7-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Capturez les points centraux dans le vide", "avec vos classes spéciales."), + en=("Capture the central points in the void", "with your special classes."), + ), + ), + ModeSpec( + id="rush_the_flag", name_fr="Rush The Flag", group="ctpf", + min_players=8, max_players=UNLIMITED, + estimated_time="5-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Volez le drapeau ennemi", "avec vos classes spéciales."), + en=("Steal the enemy flag", "with your special classes."), + ), + ), + ModeSpec( + id="spleef", name_fr="Spleef", + min_players=1, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="everywhere", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Creusez sous vos adversaires", "pour les faire tomber !"), + en=("Dig below your opponents", "to make them fall!"), + ), + ), + ModeSpec( + id="de_a_coudre", name_fr="Dé à Coudre", group="a_coudre", + min_players=1, max_players=UNLIMITED, + estimated_time="3-6 mins", inspiration="OneCube point system", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Affrontez les autres joueurs dans un saut,", "dans une piscine, iconique de Minecraft !"), + en=("Take on the other players in", "Minecraft's iconic pool jump!"), + ), + ), + ModeSpec( + id="feed_fast", name_fr="Feed Fast", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="Le diabète", suggested_by="Arobaze", developed_by="LTHCTheMaster", + description=Localized( + fr=("Mangez le plus de nourriture", "possible pour gagner !"), + en=("Eat the most food", "possible to win!"), + ), + ), + ModeSpec( + id="mlg_a_coudre", name_fr="MLG à Coudre", group="a_coudre", + min_players=1, max_players=8, + estimated_time="1-6 mins", inspiration="Dé à Coudre", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Sautez et utilisez votre seau d'eau", "pour survivre à la chute !"), + en=("Jump and use your water bucket", "to survive the fall!"), + ), + ), + ModeSpec( + id="protect_the_king", name_fr="Protect The King", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Charlemagne", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Tuez le roi ennemi ou", "protégez le vôtre !"), + en=("Kill the enemy king or", "protect your own!"), + ), + ), + ModeSpec( + id="castagne", name_fr="Castagne RP", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="Domestic Violence", suggested_by="JustNoob", developed_by="Stoupy", + description=Localized( + fr=("Battez-vous au corps à corps", "et soyez le dernier survivant !"), + en=("Fight hand-to-hand combat", "and be the last survivor!"), + ), + ), + ModeSpec( + id="layers_2_teams", name_fr="Layers 2 Teams", group="layers", + min_players=2, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Équipez-vous rapidement et détruisez", "l'équipe adverse située de l'autre côté du mur !"), + en=("Gear up quickly and destroy the enemy team", "on the other side of the wall!"), + ), + ), + ModeSpec( + id="layers_4_teams", name_fr="Layers 4 Teams", group="layers", + min_players=4, max_players=UNLIMITED, + estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Course d'équipement entre", "4 équipes, éliminez vos ennemis !"), + en=("Equipment race between", "4 teams, eliminate your enemies!"), + ), + ), + ModeSpec( + id="thunder_spear", name_fr="Thunder Spear", + min_players=2, max_players=UNLIMITED, + estimated_time="2m30s", inspiration="SNK/Attack On Titan", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Utilisez votre équipement tridimensionnel", "et vos lances explosives !"), + en=("Use your three-dimensional maneuver gear", "and explosive spears!"), + ), + ), + ModeSpec( + id="tnt_run", name_fr="TNT Run", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Grian (2013)", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Restez en mouvement, les blocs", "disparaissent sous vos pieds !"), + en=("Keep moving, blocks disappear", "under your feet!"), + ), + ), + ModeSpec( + id="sheepwars", name_fr="SheepWars", + min_players=2, max_players=UNLIMITED, + estimated_time="2-6 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Lancez des moutons explosifs", "sur l'équipe ennemie !"), + en=("Launch explosive sheep", "at the enemy team!"), + ), + ), + ModeSpec( + id="block_party", name_fr="Block Party", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="HiveMC", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Quand la musique s'arrête,", "trouvez la bonne couleur !"), + en=("When the music stops,", "find the right color!"), + ), + ), + ModeSpec( + id="moutron", name_fr="Moutron", + min_players=2, max_players=16, + estimated_time="1-3 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Montez un mouton qui laisse", "une traînée mortelle !"), + en=("Ride a sheep that leaves", "a deadly trail!"), + ), + ), + ModeSpec( + id="snowball_painter", name_fr="Snowball Painter", + min_players=2, max_players=45, + estimated_time="45s", inspiration="Party Games/Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Peignez le plus de blocs possible", "avec vos boules de neige !"), + en=("Paint the most blocks possible", "with your snowballs!"), + ), + ), + ModeSpec( + id="gay_shooter", name_fr="Guy Shooter", + min_players=2, max_players=UNLIMITED, + estimated_time="1-5 mins", inspiration="a Survisland Event", suggested_by="GoldVision & scartvol", developed_by="Stoupy", + description=Localized( + fr=("Archers contre grimpeurs :", "empêchez-les d'atteindre le sommet !"), + en=("Archers versus climbers:", "prevent them from reaching the top!"), + ), + ), + ModeSpec( + id="build_battle", name_fr="Build Battle", + min_players=2, max_players=UNLIMITED, + estimated_time="5-7 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Construisez sur un thème", "et votez pour le meilleur build !"), + en=("Build on a theme and", "vote for the best build!"), + ), + ), + ModeSpec( + id="beat_the_kings", name_fr="Beat The Kings", + min_players=2, max_players=UNLIMITED, + estimated_time="3-5 mins", inspiration="Henry IV/Louis XVI", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=("Révolutionnaires contre rois :", "qui survivra à la bataille ?"), + en=("Revolutionaries versus kings:", "who will survive the battle?"), + ), + ), + ModeSpec( + id="coin_chaser", name_fr="Coin Chaser", + min_players=1, max_players=UNLIMITED, + estimated_time="1m30s", inspiration="Expiration", suggested_by="Ikinox", developed_by="Stoupy", + description=Localized( + fr=("Collectez le plus d'or possible", "avec vos élytras !"), + en=("Collect the most gold possible", "with your elytra!"), + ), + ), + ModeSpec( + id="murder_mystery", name_fr="Murder Mystery", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="Hypixel", suggested_by="GoldVision", developed_by="Stoupy", + description=Localized( + fr=("Un meurtrier, un détective,", "et des innocents dans une map pleine d'or !"), + en=("A murderer, a detective,", "and innocents in a map full of gold!"), + ), + ), + ModeSpec( + id="panic_chase", name_fr="Panic Chase", + min_players=4, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Le chat et la souris", suggested_by="Arti", developed_by="Stoupy", + description=Localized( + fr=("Survivez aux chasseurs dans", "une petite arène fermée !"), + en=("Survive the hunters in", "a small closed arena!"), + ), + ), + ModeSpec( + id="simultaneous_jump", name_fr="Simultaneous Jump", group="a_coudre", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="Dé à Coudre/MLG à Coudre", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Dé à coudre ou MLG à coudre,", "mais tout le monde saute en même temps !"), + en=("Pool jump or MLG water bucket,", "but everyone jumps at the same time!"), + ), + ), + ModeSpec( + id="bombardement", name_fr="Bombardment", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Esquivez les tirs de ce bâteau volant", "afin d'être le dernier survivant !"), + en=("Dodge the shots from this flying boat", "to be the last survivor!"), + ), + ), + ModeSpec( + id="shoot_da_sheep", name_fr="Shoot Da'Sheep", + min_players=1, max_players=UNLIMITED, + estimated_time="60s", inspiration="BrawlMC", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=( + "Des moutons tombent du ciel, certains donnent plus de points", + "que d'autres, ayez le plus de points à la fin !", + ), + en=( + "Sheeps are falling from the sky, some give more points", + "than others, get the most points at the end!", + ), + ), + ), + ModeSpec( + id="minigolf", name_fr="MiniGolf", + min_players=1, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="The physical world", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Soyez celui qui réussi à faire tomber sa balle dans", "le trou avec le moins de coup possible !"), + en=("Be the one to drop your golf ball in", "the hole with the fewest strokes possible!"), + ), + ), + ModeSpec( + id="cigogne", name_fr="Cigogne", + min_players=2, max_players=UNLIMITED, + estimated_time="1-4 mins", inspiration="ShoupZ/JustNoob", suggested_by="GoldVision", developed_by="Stoupy", + description=Localized( + fr=("Combat de spectres puissants", "dans une salle fermée !"), + en=("Powerful spectres battle", "in a closed room!"), + ), + ), + ModeSpec( + id="infected", name_fr="Infected", + min_players=8, max_players=UNLIMITED, + estimated_time="5-10 mins", inspiration="ImagineYourCraft (2013)", suggested_by="the whole Earth", developed_by="Stoupy", + description=Localized( + fr=( + "Un infecté est choisi en début de partie et doit infecter", + "tous les humains qui doivent attendre les renforts !", + ), + en=( + "An infected person is chosen at the beginning of the game and", + "must infect all humans who must wait for rescue!", + ), + ), + ), + ModeSpec( + id="glassrunner", name_fr="GlassRunner", group="ctpf", + min_players=2, max_players=UNLIMITED, + estimated_time="8-10 mins", inspiration="Épicube", suggested_by="AirDox", developed_by="AirDox", + description=Localized( + fr=( + "Affrontez l'équipe adverse dans le ciel dans un", + "CaptureThePoint assaisonné de multiples particularités", + ), + en=( + "Take on the opposing team in the sky in a", + "CaptureThePoint seasoned with multiple special features", + ), + ), + ), + ModeSpec( + id="capture_the_flag", name_fr="Capture The Flag", group="ctpf", + min_players=10, max_players=UNLIMITED, + estimated_time="10-15 mins", inspiration="Aspiration", suggested_by="Oraclette", developed_by="Stoupy", + description=Localized( + fr=( + "Deux équipes doivent s'affronter pour capturer", + "le drapeau ennemi grâce à des", + "classes spécifiques différentes", + ), + en=("Two teams must compete to capture the enemy flag", "with the help of different specific classes"), + ), + ), + ModeSpec( + id="memory_mine", name_fr="Memory Mine", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="Pico Party", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Mémorisez la position des mines", "qui clignotent et disparaissent !"), + en=("Memorize the position of mines", "that blink and disappear!"), + ), + ), + ModeSpec( + id="fish_fight", name_fr="Fish Fight", + min_players=2, max_players=UNLIMITED, + estimated_time="2-4 mins", inspiration="Findus", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=("Éjectez vos adversaires", "avec votre poisson !"), + en=("Eject your opponents", "with your fish!"), + ), + ), + ModeSpec( + id="one_shot", name_fr="One Shot", + min_players=2, max_players=10, + estimated_time="2-4 mins", inspiration="OneShot", suggested_by="Luxio", developed_by="Luxio", + description=Localized( + fr=( + "Éliminez vos adversaires grâce à votre arc", + "surpuissant et votre épée pour remporter la victoire !", + ), + en=("Eliminate your opponents with your", "powerful bow and sword to claim victory!"), + ), + ), + ModeSpec( + id="pillars_of_fortune", name_fr="Pillars of Fortune", + min_players=2, max_players=UNLIMITED, + estimated_time="1-6 mins", inspiration="Cubecraft", suggested_by="TreekoZ", developed_by="Stoupy", + description=Localized( + fr=("Recevez des objets aléatoires", "sur de grands piliers !"), + en=("Receive random items", "on large pillars!"), + ), + ), + ModeSpec( + id="fireblast", name_fr="Fireblast", + min_players=2, max_players=UNLIMITED, + estimated_time="1-2 mins", inspiration="LifeCraft", suggested_by="Stoupy", developed_by="Stoupy", + description=Localized( + fr=("Éjectez tous les joueurs de la plateforme", "en utilisant des boules de feu explosives !"), + en=("Knock out every players from the platform", "by using explosive fireballs!"), + ), + ), + # ModeSpec( + # id="pvpswap", name_fr="PvPSwap", + # min_players=2, max_players=32, + # estimated_time="1-2 mins", inspiration="Épicube", suggested_by="Arobaze", developed_by="Arobaze", + # description=Localized( + # fr=("Éliminez les autres joueurs au travers", "des changements inopinés de positions !"), + # en=("Eliminate other players through", "sudden changes in positions!"), + # ), + # ), +) + +# Vote groups: modes sharing a "group" show up as a single entry, and a second vote picks one +GROUPS: tuple[GroupSpec, ...] = ( + GroupSpec( + id="a_coudre", name_fr="Dé À Coudre & Co", estimated_time="1-6 mins", + description=Localized( + fr=("Sautez dans la piscine, avec ou sans seau d'eau,", "tous en même temps ou chacun son tour !"), + en=("Jump into the pool, with or without a water bucket,", "all at once or one at a time!"), + ), + ), + GroupSpec( + id="creepers", name_fr="Creepers", estimated_time="1m30s", + description=Localized( + fr=("Survivez aux creepers, en les repoussant", "ou en fuyant leurs explosions !"), + en=("Survive the creepers, by pushing them away", "or fleeing their explosions!"), + ), + ), + GroupSpec( + id="layers", name_fr="Layers", estimated_time="4-10 mins", + description=Localized( + fr=("Équipez-vous rapidement et détruisez", "les équipes adverses !"), + en=("Gear up quickly and destroy", "the enemy teams!"), + ), + ), + GroupSpec( + id="ctpf", name_fr="Capture the Point/Flag", estimated_time="5-15 mins", + description=Localized( + fr=("Affrontez l'équipe adverse : capturez des points,", "volez des drapeaux ou courez sur le verre !"), + en=("Face off against the enemy team: capture points,", "steal flags or run across the glass!"), + ), + ), +) + +GROUPS_BY_ID: dict[str, GroupSpec] = {group.id: group for group in GROUPS} +""" Lookup used when building the storage entry of a multi-mode group. """ + diff --git a/src/datapack/modes/definitions.py b/src/datapack/modes/definitions.py index 3a9a15a6d..3d3b6d9d7 100644 --- a/src/datapack/modes/definitions.py +++ b/src/datapack/modes/definitions.py @@ -1,7 +1,7 @@ -""" Declarative catalogue of every minigame and vote group, and the storage entries built from it. +""" Storage entries built from the minigame catalogue. -MODES drives the vote message, the lore, the starting pop-ups and the stats objectives. A mode -generates even when its entry is commented out here: only its presence in the vote depends on it. +Turns the declarative MODES and GROUPS tables into the "minigames" and "groups" lists that the +vote, the lore and the starting pop-ups read at runtime. """ # Imports import json @@ -9,7 +9,8 @@ from stewbeet import Mem, TextComponent, write_function from stouputils.typing import JsonDict -from .spec import UNLIMITED, GroupSpec, Localized, ModeSpec +from .catalogue import GROUPS_BY_ID, MODES +from .spec import GroupSpec, ModeSpec # Constants GROUP_NAME_COLOR: str = "#FFCC00" @@ -28,487 +29,6 @@ """ Extra lore labels for a multi-mode group, per language. """ -# Minigames -MODES: tuple[ModeSpec, ...] = ( - ModeSpec( - id="pitch_creep", name_fr="Pitch Creep", group="creepers", - min_players=1, max_players=UNLIMITED, - estimated_time="1m30s", inspiration="Pitchout/Épicube", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Survivez en repoussant les creepers", "dans une arène fermée."), - en=("Survive by pushing away creepers", "in a closed arena."), - ), - ), - ModeSpec( - id="laser_game", name_fr="Laser Game", - min_players=2, max_players=UNLIMITED, - estimated_time="2m30s", inspiration="The physical world", suggested_by="Vigo", developed_by="Stoupy", - description=Localized( - fr=( - "Affrontez l'équipe adverse grâce à vos fusils-laser et faites", - "le plus d'élimination possible pendant un temps imparti !", - ), - en=( - "Battle the enemy team with your laser rifles and get", - "as many takedowns as possible within the allotted time!", - ), - ), - ), - ModeSpec( - id="warden_escape", name_fr="Warden Escape", - min_players=1, max_players=UNLIMITED, - estimated_time="1m30s", inspiration="Minecraft 1.19", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Survivez aux wardens dans", "une arène fermée."), - en=("Survive the wardens in", "a closed arena."), - ), - ), - ModeSpec( - id="pitchout", name_fr="Pitchout", - min_players=2, max_players=UNLIMITED, - estimated_time="1-5 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Poussez vos adversaires dans l'eau", "et soyez le dernier survivant !"), - en=("Push your opponents into the water", "and be the last survivor!"), - ), - ), - ModeSpec( - id="creeper_apocalypse", name_fr="Creeper Apocalypse", group="creepers", - min_players=1, max_players=UNLIMITED, - estimated_time="1m30s", inspiration="Expiration", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Survivez aux creepers qui se", "multiplient et explosent."), - en=("Survive the creepers that", "duplicate and explode."), - ), - ), - ModeSpec( - id="traitors_game", name_fr="Traitors Game", - min_players=4, max_players=UNLIMITED, - estimated_time="4-10 mins", inspiration="Taupe Gun UHC", suggested_by="Luxio", developed_by="Stoupy", - description=Localized( - fr=( - "Des traitres se cachent parmi les joueurs.", - "Innocents contre Traitres, quel camp va-t-il gagner ?", - ), - en=("Traitors are hinding among players.", "Innocents versus Traitors, which side will win?"), - ), - ), - # ModeSpec( - # id="boat_race", name_fr="Boat Race", - # min_players=1, max_players=UNLIMITED, - # estimated_time="3-8 mins", inspiration="Expiration", suggested_by="Stoupy", developed_by="LTHCTheMaster", - # description=Localized( - # fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), - # en=("Race for reaching the finish line", "in first to claim the win!"), - # ), - # ), - ModeSpec( - id="spectres_game", name_fr="Spectres Game", - min_players=2, max_players=UNLIMITED, - estimated_time="4-10 mins", inspiration="Expiration", suggested_by="Luxio", developed_by="Stoupy", - description=Localized( - fr=( - "Deux camps : un avec que des joueurs visibles,", - "l'autre avec des spectres, quel camp va-t-il gagner ?", - ), - en=("Two sides: one with visible players,", "the other with spectres, which side will win?"), - ), - ), - ModeSpec( - id="race", name_fr="Race", - min_players=1, max_players=UNLIMITED, - estimated_time="3-6 mins", inspiration="Racing games", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Battez-vous pour atteindre la ligne d'arrivée", "en premier pour remporter la partie !"), - en=("Race for reaching the finish line", "in first to claim the win!"), - ), - ), - ModeSpec( - id="rush_the_point", name_fr="Rush The Point", group="ctpf", - min_players=8, max_players=UNLIMITED, - estimated_time="7-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Capturez les points centraux dans le vide", "avec vos classes spéciales."), - en=("Capture the central points in the void", "with your special classes."), - ), - ), - ModeSpec( - id="rush_the_flag", name_fr="Rush The Flag", group="ctpf", - min_players=8, max_players=UNLIMITED, - estimated_time="5-10 mins", inspiration="PluginLess", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Volez le drapeau ennemi", "avec vos classes spéciales."), - en=("Steal the enemy flag", "with your special classes."), - ), - ), - ModeSpec( - id="spleef", name_fr="Spleef", - min_players=1, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="everywhere", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Creusez sous vos adversaires", "pour les faire tomber !"), - en=("Dig below your opponents", "to make them fall!"), - ), - ), - ModeSpec( - id="de_a_coudre", name_fr="Dé à Coudre", group="a_coudre", - min_players=1, max_players=UNLIMITED, - estimated_time="3-6 mins", inspiration="OneCube point system", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Affrontez les autres joueurs dans un saut,", "dans une piscine, iconique de Minecraft !"), - en=("Take on the other players in", "Minecraft's iconic pool jump!"), - ), - ), - ModeSpec( - id="feed_fast", name_fr="Feed Fast", - min_players=1, max_players=UNLIMITED, - estimated_time="60s", inspiration="Le diabète", suggested_by="Arobaze", developed_by="LTHCTheMaster", - description=Localized( - fr=("Mangez le plus de nourriture", "possible pour gagner !"), - en=("Eat the most food", "possible to win!"), - ), - ), - ModeSpec( - id="mlg_a_coudre", name_fr="MLG à Coudre", group="a_coudre", - min_players=1, max_players=8, - estimated_time="1-6 mins", inspiration="Dé à Coudre", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Sautez et utilisez votre seau d'eau", "pour survivre à la chute !"), - en=("Jump and use your water bucket", "to survive the fall!"), - ), - ), - ModeSpec( - id="protect_the_king", name_fr="Protect The King", - min_players=2, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="Charlemagne", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Tuez le roi ennemi ou", "protégez le vôtre !"), - en=("Kill the enemy king or", "protect your own!"), - ), - ), - ModeSpec( - id="castagne", name_fr="Castagne RP", - min_players=2, max_players=UNLIMITED, - estimated_time="1-5 mins", inspiration="Domestic Violence", suggested_by="JustNoob", developed_by="Stoupy", - description=Localized( - fr=("Battez-vous au corps à corps", "et soyez le dernier survivant !"), - en=("Fight hand-to-hand combat", "and be the last survivor!"), - ), - ), - ModeSpec( - id="layers_2_teams", name_fr="Layers 2 Teams", group="layers", - min_players=2, max_players=UNLIMITED, - estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Équipez-vous rapidement et détruisez", "l'équipe adverse située de l'autre côté du mur !"), - en=("Gear up quickly and destroy the enemy team", "on the other side of the wall!"), - ), - ), - ModeSpec( - id="layers_4_teams", name_fr="Layers 4 Teams", group="layers", - min_players=4, max_players=UNLIMITED, - estimated_time="4-10 mins", inspiration="PenguinGames", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Course d'équipement entre", "4 équipes, éliminez vos ennemis !"), - en=("Equipment race between", "4 teams, eliminate your enemies!"), - ), - ), - ModeSpec( - id="thunder_spear", name_fr="Thunder Spear", - min_players=2, max_players=UNLIMITED, - estimated_time="2m30s", inspiration="SNK/Attack On Titan", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Utilisez votre équipement tridimensionnel", "et vos lances explosives !"), - en=("Use your three-dimensional maneuver gear", "and explosive spears!"), - ), - ), - ModeSpec( - id="tnt_run", name_fr="TNT Run", - min_players=2, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="Grian (2013)", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Restez en mouvement, les blocs", "disparaissent sous vos pieds !"), - en=("Keep moving, blocks disappear", "under your feet!"), - ), - ), - ModeSpec( - id="sheepwars", name_fr="SheepWars", - min_players=2, max_players=UNLIMITED, - estimated_time="2-6 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Lancez des moutons explosifs", "sur l'équipe ennemie !"), - en=("Launch explosive sheep", "at the enemy team!"), - ), - ), - ModeSpec( - id="block_party", name_fr="Block Party", - min_players=2, max_players=UNLIMITED, - estimated_time="1-5 mins", inspiration="HiveMC", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Quand la musique s'arrête,", "trouvez la bonne couleur !"), - en=("When the music stops,", "find the right color!"), - ), - ), - ModeSpec( - id="moutron", name_fr="Moutron", - min_players=2, max_players=16, - estimated_time="1-3 mins", inspiration="Épicube", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Montez un mouton qui laisse", "une traînée mortelle !"), - en=("Ride a sheep that leaves", "a deadly trail!"), - ), - ), - ModeSpec( - id="snowball_painter", name_fr="Snowball Painter", - min_players=2, max_players=45, - estimated_time="45s", inspiration="Party Games/Hypixel", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Peignez le plus de blocs possible", "avec vos boules de neige !"), - en=("Paint the most blocks possible", "with your snowballs!"), - ), - ), - ModeSpec( - id="gay_shooter", name_fr="Guy Shooter", - min_players=2, max_players=UNLIMITED, - estimated_time="1-5 mins", inspiration="a Survisland Event", suggested_by="GoldVision & scartvol", developed_by="Stoupy", - description=Localized( - fr=("Archers contre grimpeurs :", "empêchez-les d'atteindre le sommet !"), - en=("Archers versus climbers:", "prevent them from reaching the top!"), - ), - ), - ModeSpec( - id="build_battle", name_fr="Build Battle", - min_players=2, max_players=UNLIMITED, - estimated_time="5-7 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Construisez sur un thème", "et votez pour le meilleur build !"), - en=("Build on a theme and", "vote for the best build!"), - ), - ), - ModeSpec( - id="beat_the_kings", name_fr="Beat The Kings", - min_players=2, max_players=UNLIMITED, - estimated_time="3-5 mins", inspiration="Henry IV/Louis XVI", suggested_by="Luxio", developed_by="Luxio", - description=Localized( - fr=("Révolutionnaires contre rois :", "qui survivra à la bataille ?"), - en=("Revolutionaries versus kings:", "who will survive the battle?"), - ), - ), - ModeSpec( - id="coin_chaser", name_fr="Coin Chaser", - min_players=1, max_players=UNLIMITED, - estimated_time="1m30s", inspiration="Expiration", suggested_by="Ikinox", developed_by="Stoupy", - description=Localized( - fr=("Collectez le plus d'or possible", "avec vos élytras !"), - en=("Collect the most gold possible", "with your elytra!"), - ), - ), - ModeSpec( - id="murder_mystery", name_fr="Murder Mystery", - min_players=2, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="Hypixel", suggested_by="GoldVision", developed_by="Stoupy", - description=Localized( - fr=("Un meurtrier, un détective,", "et des innocents dans une map pleine d'or !"), - en=("A murderer, a detective,", "and innocents in a map full of gold!"), - ), - ), - ModeSpec( - id="panic_chase", name_fr="Panic Chase", - min_players=4, max_players=UNLIMITED, - estimated_time="1-2 mins", inspiration="Le chat et la souris", suggested_by="Arti", developed_by="Stoupy", - description=Localized( - fr=("Survivez aux chasseurs dans", "une petite arène fermée !"), - en=("Survive the hunters in", "a small closed arena!"), - ), - ), - ModeSpec( - id="simultaneous_jump", name_fr="Simultaneous Jump", group="a_coudre", - min_players=1, max_players=UNLIMITED, - estimated_time="60s", inspiration="Dé à Coudre/MLG à Coudre", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Dé à coudre ou MLG à coudre,", "mais tout le monde saute en même temps !"), - en=("Pool jump or MLG water bucket,", "but everyone jumps at the same time!"), - ), - ), - ModeSpec( - id="bombardement", name_fr="Bombardment", - min_players=2, max_players=UNLIMITED, - estimated_time="1-2 mins", inspiration="Hypixel", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Esquivez les tirs de ce bâteau volant", "afin d'être le dernier survivant !"), - en=("Dodge the shots from this flying boat", "to be the last survivor!"), - ), - ), - ModeSpec( - id="shoot_da_sheep", name_fr="Shoot Da'Sheep", - min_players=1, max_players=UNLIMITED, - estimated_time="60s", inspiration="BrawlMC", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=( - "Des moutons tombent du ciel, certains donnent plus de points", - "que d'autres, ayez le plus de points à la fin !", - ), - en=( - "Sheeps are falling from the sky, some give more points", - "than others, get the most points at the end!", - ), - ), - ), - ModeSpec( - id="minigolf", name_fr="MiniGolf", - min_players=1, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="The physical world", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Soyez celui qui réussi à faire tomber sa balle dans", "le trou avec le moins de coup possible !"), - en=("Be the one to drop your golf ball in", "the hole with the fewest strokes possible!"), - ), - ), - ModeSpec( - id="cigogne", name_fr="Cigogne", - min_players=2, max_players=UNLIMITED, - estimated_time="1-4 mins", inspiration="ShoupZ/JustNoob", suggested_by="GoldVision", developed_by="Stoupy", - description=Localized( - fr=("Combat de spectres puissants", "dans une salle fermée !"), - en=("Powerful spectres battle", "in a closed room!"), - ), - ), - ModeSpec( - id="infected", name_fr="Infected", - min_players=8, max_players=UNLIMITED, - estimated_time="5-10 mins", inspiration="ImagineYourCraft (2013)", suggested_by="the whole Earth", developed_by="Stoupy", - description=Localized( - fr=( - "Un infecté est choisi en début de partie et doit infecter", - "tous les humains qui doivent attendre les renforts !", - ), - en=( - "An infected person is chosen at the beginning of the game and", - "must infect all humans who must wait for rescue!", - ), - ), - ), - ModeSpec( - id="glassrunner", name_fr="GlassRunner", group="ctpf", - min_players=2, max_players=UNLIMITED, - estimated_time="8-10 mins", inspiration="Épicube", suggested_by="AirDox", developed_by="AirDox", - description=Localized( - fr=( - "Affrontez l'équipe adverse dans le ciel dans un", - "CaptureThePoint assaisonné de multiples particularités", - ), - en=( - "Take on the opposing team in the sky in a", - "CaptureThePoint seasoned with multiple special features", - ), - ), - ), - ModeSpec( - id="capture_the_flag", name_fr="Capture The Flag", group="ctpf", - min_players=10, max_players=UNLIMITED, - estimated_time="10-15 mins", inspiration="Aspiration", suggested_by="Oraclette", developed_by="Stoupy", - description=Localized( - fr=( - "Deux équipes doivent s'affronter pour capturer", - "le drapeau ennemi grâce à des", - "classes spécifiques différentes", - ), - en=("Two teams must compete to capture the enemy flag", "with the help of different specific classes"), - ), - ), - ModeSpec( - id="memory_mine", name_fr="Memory Mine", - min_players=2, max_players=UNLIMITED, - estimated_time="1-2 mins", inspiration="Pico Party", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Mémorisez la position des mines", "qui clignotent et disparaissent !"), - en=("Memorize the position of mines", "that blink and disappear!"), - ), - ), - ModeSpec( - id="fish_fight", name_fr="Fish Fight", - min_players=2, max_players=UNLIMITED, - estimated_time="2-4 mins", inspiration="Findus", suggested_by="Luxio", developed_by="Luxio", - description=Localized( - fr=("Éjectez vos adversaires", "avec votre poisson !"), - en=("Eject your opponents", "with your fish!"), - ), - ), - ModeSpec( - id="one_shot", name_fr="One Shot", - min_players=2, max_players=10, - estimated_time="2-4 mins", inspiration="OneShot", suggested_by="Luxio", developed_by="Luxio", - description=Localized( - fr=( - "Éliminez vos adversaires grâce à votre arc", - "surpuissant et votre épée pour remporter la victoire !", - ), - en=("Eliminate your opponents with your", "powerful bow and sword to claim victory!"), - ), - ), - ModeSpec( - id="pillars_of_fortune", name_fr="Pillars of Fortune", - min_players=2, max_players=UNLIMITED, - estimated_time="1-6 mins", inspiration="Cubecraft", suggested_by="TreekoZ", developed_by="Stoupy", - description=Localized( - fr=("Recevez des objets aléatoires", "sur de grands piliers !"), - en=("Receive random items", "on large pillars!"), - ), - ), - ModeSpec( - id="fireblast", name_fr="Fireblast", - min_players=2, max_players=UNLIMITED, - estimated_time="1-2 mins", inspiration="LifeCraft", suggested_by="Stoupy", developed_by="Stoupy", - description=Localized( - fr=("Éjectez tous les joueurs de la plateforme", "en utilisant des boules de feu explosives !"), - en=("Knock out every players from the platform", "by using explosive fireballs!"), - ), - ), - # ModeSpec( - # id="pvpswap", name_fr="PvPSwap", - # min_players=2, max_players=32, - # estimated_time="1-2 mins", inspiration="Épicube", suggested_by="Arobaze", developed_by="Arobaze", - # description=Localized( - # fr=("Éliminez les autres joueurs au travers", "des changements inopinés de positions !"), - # en=("Eliminate other players through", "sudden changes in positions!"), - # ), - # ), -) - -# Vote groups: modes sharing a "group" show up as a single entry, and a second vote picks one -GROUPS: tuple[GroupSpec, ...] = ( - GroupSpec( - id="a_coudre", name_fr="Dé À Coudre & Co", estimated_time="1-6 mins", - description=Localized( - fr=("Sautez dans la piscine, avec ou sans seau d'eau,", "tous en même temps ou chacun son tour !"), - en=("Jump into the pool, with or without a water bucket,", "all at once or one at a time!"), - ), - ), - GroupSpec( - id="creepers", name_fr="Creepers", estimated_time="1m30s", - description=Localized( - fr=("Survivez aux creepers, en les repoussant", "ou en fuyant leurs explosions !"), - en=("Survive the creepers, by pushing them away", "or fleeing their explosions!"), - ), - ), - GroupSpec( - id="layers", name_fr="Layers", estimated_time="4-10 mins", - description=Localized( - fr=("Équipez-vous rapidement et détruisez", "les équipes adverses !"), - en=("Gear up quickly and destroy", "the enemy teams!"), - ), - ), - GroupSpec( - id="ctpf", name_fr="Capture the Point/Flag", estimated_time="5-15 mins", - description=Localized( - fr=("Affrontez l'équipe adverse : capturez des points,", "volez des drapeaux ou courez sur le verre !"), - en=("Face off against the enemy team: capture points,", "steal flags or run across the glass!"), - ), - ), -) - -GROUPS_BY_ID: dict[str, GroupSpec] = {group.id: group for group in GROUPS} -""" Lookup used when building the storage entry of a multi-mode group. """ - # Functions def mode_lore(mode: ModeSpec, lang: str) -> list[TextComponent]: diff --git a/src/datapack/survival_maps/utils.py b/src/datapack/survival_maps/utils.py index a2c4e018d..bb5d3fd41 100644 --- a/src/datapack/survival_maps/utils.py +++ b/src/datapack/survival_maps/utils.py @@ -8,7 +8,7 @@ from beet import Function from stewbeet.core import Mem, write_function -from ..modes.definitions import MODES +from ..modes.catalogue import MODES from .shared_memory import SharedMemory, generated_maps, survival_maps diff --git a/src/validation.py b/src/validation.py index 438b1fb6a..c339dbba0 100644 --- a/src/validation.py +++ b/src/validation.py @@ -14,7 +14,8 @@ import stouputils as stp from stewbeet import Mem -from .datapack.modes.definitions import GROUPS, GROUPS_BY_ID, MODES, grouped_modes +from .datapack.modes.catalogue import GROUPS, GROUPS_BY_ID, MODES +from .datapack.modes.definitions import grouped_modes from .datapack.survival_maps.shared_memory import generated_maps # Constants diff --git a/tools/check_conventions.py b/tools/check_conventions.py new file mode 100644 index 000000000..fb702886a --- /dev/null +++ b/tools/check_conventions.py @@ -0,0 +1,158 @@ +""" Enforce the architectural rules that ruff and pyright cannot express. + +Static only: reads the sources, never builds. The build based guards live in +check_output_drift.py and report_merged_functions.py. +""" +# Imports +import ast +import re +import sys +from pathlib import Path + +import stouputils as stp + +# Constants +SRC: Path = Path(__file__).resolve().parent.parent / "src" +""" Root of the project sources. """ + +MODES: Path = SRC / "datapack" / "modes" +""" Folder holding one package per minigame. """ + +MAX_LINES: int = 300 +""" A file past this length becomes a package, so that finding things stays a matter of folders. """ + +LONG_FILE_EXCEPTIONS: tuple[str, ...] = ("datapack/modes/*/main.py", "datapack/modes/catalogue.py") +""" Files allowed to grow past MAX_LINES, as posix glob patterns relative to src/. + +Both are single tables read with Ctrl+F: a mode's main.py holds all of that game's logic, and the +catalogue holds every mode declaration. Splitting either would only add folders to search through. +""" + +LONG_FILE_DEBT: frozenset[str] = frozenset({ + "datapack/cinematic/main.py", + "datapack/engine/main.py", + "datapack/kits/model.py", + "datapack/maps/main.py", + "datapack/player/jump_timer.py", + "datapack/player/layout.py", + "datapack/player/main.py", + "datapack/player/practice.py", + "datapack/player/translations.py", + "datapack/root/main.py", + "datapack/shop/utils.py", + "datapack/stats/main.py", + "datapack/survival_maps/utils.py", + "resource_pack/shaders.py", +}) +""" Files already past MAX_LINES, to be split into packages. + +This ledger may only shrink: a file leaving it must be removed from here, and no file may be +added. It exists so the rule can be enforced today instead of after the whole split is done. +""" + +MODEL_FILES: tuple[str, ...] = ("datapack/modes/spec.py",) +""" Files that describe the domain and must stay free of untyped containers. """ + +UNTYPED_NAMES: frozenset[str] = frozenset({"Any", "JsonDict", "JsonList"}) +""" Containers that defeat the type checker, banned from the model files. """ + +DOWNWARD_IMPORT: re.Pattern[str] = re.compile(r"^\s*from\s+\.+modes\.([a-z0-9_]+)\.", re.MULTILINE) +""" Import of a specific mode from outside the modes package, ex: `from ..modes.spleef.shop import`. """ + + +# Classes +class Conventions: + + @staticmethod + def python_files() -> list[Path]: + """ Every source file of the project. + + Returns: + list[Path]: Paths to each .py file under src/ + """ + return sorted(path for path in SRC.rglob("*.py") if "__pycache__" not in path.parts) + + @staticmethod + def relative(path: Path) -> str: + """ Path of a source file relative to src/, with forward slashes. + + Args: + path (Path): The file + Returns: + str: The relative path, ex: "datapack/modes/tnt_run/main.py" + """ + return path.relative_to(SRC).as_posix() + + @staticmethod + def check_length() -> list[str]: + """ No file grows past MAX_LINES, outside of the documented exceptions. + + Returns: + list[str]: One message per offending file + """ + problems: list[str] = [] + for path in Conventions.python_files(): + relative: str = Conventions.relative(path) + if any(Path(relative).match(pattern) for pattern in LONG_FILE_EXCEPTIONS): + continue + length: int = len(path.read_text(encoding="utf-8").splitlines()) + if length > MAX_LINES and relative not in LONG_FILE_DEBT: + problems.append(f"{relative} is {length} lines, split it into a package (limit {MAX_LINES})") + elif length <= MAX_LINES and relative in LONG_FILE_DEBT: + problems.append(f"{relative} is now {length} lines, remove it from LONG_FILE_DEBT") + return problems + + @staticmethod + def check_model_purity() -> list[str]: + """ The model files describe data with real types, never with untyped containers. + + Returns: + list[str]: One message per offending name + """ + problems: list[str] = [] + for relative in MODEL_FILES: + source: str = (SRC / relative).read_text(encoding="utf-8") + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Name) and node.id in UNTYPED_NAMES: + problems.append(f"{relative}:{node.lineno} uses '{node.id}', the model must stay fully typed") + return problems + + @staticmethod + def check_no_downward_import() -> list[str]: + """ No subsystem imports a named minigame: it consumes the registry instead. + + Returns: + list[str]: One message per offending import + """ + problems: list[str] = [] + for path in Conventions.python_files(): + if MODES in path.parents: + continue + for mode in DOWNWARD_IMPORT.findall(path.read_text(encoding="utf-8")): + problems.append( + f"{Conventions.relative(path)} imports mode '{mode}' directly, " + f"consume the registry so adding a mode stays a one folder change") + return problems + + @staticmethod + def main() -> int: + """ Run every convention check. + + Returns: + int: Process exit code, 0 when the sources follow the rules + """ + problems: list[str] = ( + Conventions.check_length() + Conventions.check_model_purity() + Conventions.check_no_downward_import()) + if not problems: + stp.info(f"{len(Conventions.python_files())} source file(s) follow the conventions") + return 0 + + stp.error(f"{len(problems)} convention problem(s):") + for problem in problems: + stp.error(f" {problem}") + return 1 + + +if __name__ == "__main__": + sys.exit(Conventions.main()) + From 13a3bbdaf038f87a342d3582d7e48ec6ad8ef1c0 Mon Sep 17 00:00:00 2001 From: Stoupy51 Date: Sat, 22 Aug 2026 14:44:44 +0200 Subject: [PATCH 10/10] =?UTF-8?q?docs:=20=F0=9F=93=9D=20README=20recal?= =?UTF-8?q?=C3=A9=20sur=20la=20nouvelle=20structure=20et=20les=20garde-fou?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2b85c46fa..b18e0893b 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,15 @@ Documents (Google Sheet) : - [Le pipeline de build](#le-pipeline-de-build) - [Où se trouve quoi ?](#où-se-trouve-quoi-) - [Ajouter un mode de jeu](#ajouter-un-mode-de-jeu) - - [1. Créer le dossier](#1-créer-le-dossier) - - [2. Écrire `main.py`](#2-écrire-mainpy) + - [1. Générer le squelette](#1-générer-le-squelette) + - [2. Remplir `main.py`](#2-remplir-mainpy) - [3. Déclarer le mode dans la liste de vote](#3-déclarer-le-mode-dans-la-liste-de-vote) - [4. Tester en jeu](#4-tester-en-jeu) - [5. Ressources supplémentaires (optionnel)](#5-ressources-supplémentaires-optionnel) - [Anatomie d'un mode](#anatomie-dun-mode) - [Ajouter autre chose](#ajouter-autre-chose) - [Conventions de code](#conventions-de-code) + - [Garde-fous](#garde-fous) - [Workflow Git](#workflow-git)
@@ -80,15 +81,17 @@ Pour retrouver le code d'une commande in-game, grep son texte dans `src/` : le c Switch/ ├── ⚙️ beet.yml # Config du projet et du pipeline de build ├── 🚀 upload.py # Publication d'une release GitHub +├── 🧰 tools/ # Scaffolding et garde-fous (voir plus bas) ├── 🐍 src/ # TOUT le code source │ ├── setup_definitions.py # Étape 1 : items, blocs, matériaux, disques │ ├── link.py # Étape 2 : appelle tous les générateurs +│ ├── validation.py # Contrôles de cohérence, échoue le build │ ├── 📦 database/ # Items et comportements de blocs │ ├── 🎨 resource_pack/ # Langues, sounds.json, shaders, textures GUI, fonts │ └── 📂 datapack/ │ ├── main.py # Définitions brutes du datapack │ ├── definitions/ # Advancements, dimensions, loot tables, prédicats, tags... -│ ├── 🎮 modes/ # Un dossier par mini-jeu +│ ├── 🎮 modes/ # Un dossier par mini-jeu, + spec/catalogue/emit │ ├── 🧠 engine/ # Vote, démarrage, arrêt, signaux vers les modes │ ├── 🧍 player/ # Layout d'inventaire, practice, jump timer │ ├── 🗺️ maps/ # Chargement des maps, checkpoints, cycles de spawn @@ -131,12 +134,12 @@ Le reste vient de plugins StewBeet : headers, constantes de scoreboard, dépenda | Je veux modifier... | Fichier | |-------------------------------------------------------------------------|---------------------------------------------------------------------------------------| -| 🗳️ La liste des mini-jeux votables, descriptions, temps estimé, auteurs | `src/datapack/modes/definitions.py` (`MODES`) | -| 🧩 Les groupes de vote (variantes sous une même entrée) | `src/datapack/modes/definitions.py` (`GROUPS_INFO`) | +| 🗳️ La liste des mini-jeux votables, descriptions, temps estimé, auteurs | `src/datapack/modes/catalogue.py` (`MODES`) | +| 🧩 Les groupes de vote (variantes sous une même entrée) | `src/datapack/modes/catalogue.py` (`GROUPS`) | | 🎮 La logique d'un mini-jeu | `src/datapack/modes//main.py` | | 🌐 Les messages FR/EN d'un mini-jeu | `src/datapack/modes//translations.py` | | 📜 Les advancements, prédicats, loot tables, structures d'un mini-jeu | `src/datapack/modes//resources.py` | -| 🔁 Un helper partagé entre plusieurs modes | `src/datapack/modes/common.py` ou `src/datapack/modes/_common/main.py` | +| 🔁 Un helper partagé entre plusieurs modes | `src/datapack/modes/emit.py` ou `src/datapack/modes/_common/main.py` | | 🧠 Le vote, le lancement, l'arrêt d'une partie, les signaux | `src/datapack/engine/main.py` | | 🌍 Les maps de jeu (dimensions, régénération, zones) | `src/datapack/survival_maps/definitions.py` | | 🏁 Les checkpoints de course et les cycles de spawn | `src/datapack/maps/main.py` | @@ -153,24 +156,30 @@ Le reste vient de plugins StewBeet : headers, constantes de scoreboard, dépenda ## Ajouter un mode de jeu -### 1. Créer le dossier +### 1. Générer le squelette -La découverte est automatique, aucun import à ajouter ailleurs. +```bash +python tools/new_mode.py mon_mode +``` + +Le mode obtenu build et se lance en jeu immédiatement. La découverte est automatique : aucun import à ajouter ailleurs. ```bash src/datapack/modes/mon_mode/ ├── __init__.py # vide ├── main.py # write_mode() -└── translations.py # write_translations(), optionnel mais recommandé +└── translations.py # write_translations() ``` -### 2. Écrire `main.py` +### 2. Remplir `main.py` + +Le fichier généré ressemble déjà à ceci, avec les six hooks branchés. Il ne reste qu'à écrire les mécaniques du jeu. ```python # Imports from stewbeet import Mem, write_function -from ..common import write_modes_calls, write_time_xp_bar +from ..emit import write_modes_calls, write_time_xp_bar from .translations import write_translations @@ -243,10 +252,11 @@ Dans `src/datapack/modes/definitions.py`, ajoutez une entrée à `MODES` : | Clé / comportement | Détail | |-----------------------|----------------------------------------------------------------------------------------------| | `id` | **Exactement** le nom du dossier | -| `max_players` | `-1` signifie qu'il n'y a pas de limite | -| `group` | Clé optionnelle : les jeux d'un même groupe forment une entrée de vote, puis un second vote départage (voir `GROUPS_INFO`) | +| `max_players` | `UNLIMITED` quand le mode n'a pas de plafond | +| `group` | Optionnel : les jeux d'un même groupe forment une entrée de vote, puis un second vote départage (voir `GROUPS`) | | Lore, index, pop-ups | Générés automatiquement à partir de cette entrée | | Entrée commentée | Le mode est **quand même généré** : testable via `_force_start` sans polluer le vote | +| Erreur de saisie | Le build échoue avec un message qui nomme le problème et suggère la valeur la plus proche | ### 4. Tester en jeu @@ -269,13 +279,13 @@ Advancements, prédicats, loot tables, item modifiers, tags ou structures vont d | `translations.py` | `write_translations()` : messages FR/EN dans `/translations/*` | `write_mode()` | | `resources.py` | `write_resources()` : advancements, prédicats, loot tables, tags, structures | automatique | | `kits.py` | Kits et classes du mode, construits avec `Kit` et `KitItem` | `write_mode()` | -| `shop.py` | Dictionnaire d'upgrades de la boutique du mode | `src/datapack/shop/shared_memory.py` | +| `shop.py` | Constante `SHOP` : les upgrades vendues par le mode | automatique | | `structures/` | Fichiers `.nbt`, enregistrés via `register_structures()` | `resources.py` | | `sounds/` | Fichiers `.ogg` propres au mode, via `register_sounds()` | `resources.py` | - **`_common/`** : fonctions partagées écrites dans `switch:modes/_common/*` (mort en spectateur, fin de partie, barre d'XP, kits communs). - **`_coupdetat/`** : pseudo-mode utilisé par le moteur, pas un mini-jeu votable. -- Les helpers Python partagés vivent dans `src/datapack/modes/common.py` : `write_modes_calls`, `write_server_announce`, `write_time_xp_bar`, `write_no_drop`, `register_structures`, `register_sounds`. +- Les helpers Python partagés vivent dans `src/datapack/modes/emit.py` : `write_modes_calls`, `write_server_announce`, `write_time_xp_bar`, `write_no_drop`, `register_structures`, `register_sounds`. - Le namespace n'est jamais écrit en dur : utilisez toujours `ns: str = Mem.ctx.project_id`.
@@ -285,7 +295,7 @@ Advancements, prédicats, loot tables, item modifiers, tags ou structures vont d - **Une map** : un `clone_survival(...)` ou `fill_survival(...)` dans `src/datapack/survival_maps/definitions.py` (coordonnées, id, nom, auteurs, `view` de la cinématique). Elle devient utilisable dans le `maps:[...]` d'un `choose_map_for`. - **Un item ou un bloc** : `Item(...)` ou `Block(...)` dans `src/database/misc_items.py`. Texture trouvée automatiquement si un `.png` du même nom existe sous `assets/textures/` ; modèle, loot table et recettes sont générés. - **Un son** : le `.ogg` dans `assets/sounds/`, ou dans le `sounds/` du mode s'il lui est propre. Voir `assets/compress_ogg.py`, `force_mono.py` et `mp3_to_ogg.py`. -- **Une boutique** : un `shop.py` sur le modèle de `spleef/shop.py`, référencé dans `SHOPS` (`src/datapack/shop/shared_memory.py`). +- **Une boutique** : un `shop.py` exposant une constante `SHOP`, sur le modèle de `spleef/shop.py`. Le registre la récupère tout seul ; son rang dans la boutique vient de `SHOP_ORDER` (`src/datapack/shop/shared_memory.py`).
@@ -296,11 +306,31 @@ Advancements, prédicats, loot tables, item modifiers, tags ou structures vont d - Code auto-explicatif : peu de commentaires, les vraies explications vont dans les docstrings. - Préférez le déclaratif et le paramétré à la duplication. Le CI lance [jscpd](https://github.com/kucherenko/jscpd) sur `src/` pour traquer le copier-coller. - Un fichier qui dépasse environ 300 lignes devient un sous-module. **Exception assumée : le `main.py` d'un mode de jeu.** Toute la logique du mode reste au même endroit, ce qui rend le Ctrl+F immédiat et évite d'avoir à deviner dans quel fichier se trouve une mécanique. `build_battle/main.py` fait 935 lignes, et c'est très bien ainsi. -- Lint : `ruff check src --fix`. +- Lint : `ruff check src tools --fix` (la config vit dans `ruff.toml`). - Perfs : le serveur tourne à 20 tps avec beaucoup de joueurs. Évitez les `@e` non filtrés dans les `tick`, préférez tags et scores.
+## Garde-fous + +Quatre outils, tous lançables à la main. Les trois premiers tournent aussi en CI. + +| Commande | Ce qu'elle garantit | +|---|---| +| `ruff check src tools` | Style et imports | +| `pyright` | Typage strict, sans `Any` | +| `python tools/check_conventions.py` | Taille des fichiers, pureté du modèle, aucun import descendant vers un mode nommé | +| `python tools/check_output_drift.py` | **Le refactoring n'a rien changé** : rebuild puis `build/` identique à HEAD | +| `python tools/report_merged_functions.py` | Aucune fonction n'est écrite par deux émetteurs sans que ce soit déclaré | + +Le plus important est `check_output_drift.py`. `write_function` **ajoute** à la suite par défaut, donc deux émetteurs visant le même chemin fusionnent silencieusement dans l'ordre d'appel. Après tout déplacement de code, un `build/` inchangé est la preuve que rien n'a bougé en jeu. + +Deux règles s'appuient sur des listes explicites qui ne doivent que diminuer : +- `LONG_FILE_DEBT` dans `tools/check_conventions.py` : les fichiers qui dépassent encore 300 lignes. +- `DECLARED_MERGES` dans `tools/report_merged_functions.py` : les fonctions volontairement construites par ajouts successifs. + +
+ ## Workflow Git - Une branche par fonctionnalité, puis Pull Request vers `main`.