From 6959030639666e7313c1e3b0176e99ddb88bcb98 Mon Sep 17 00:00:00 2001 From: MeBeBruno Date: Sun, 9 Aug 2026 16:42:58 +0200 Subject: [PATCH] Delete all --- README.md | 9 - application.py | 653 ----------------------------------- gunicorn.conf.py | 2 - requirements.txt | 11 - rules.md | 91 ----- static/behavior.js | 335 ------------------ static/logo.svg | 2 - static/manifest.json | 15 - static/service_worker.js | 23 -- static/stylesheet.css | 443 ------------------------ templates/administrator.html | 333 ------------------ templates/game.html | 187 ---------- templates/layout.html | 33 -- templates/login.html | 72 ---- 14 files changed, 2209 deletions(-) delete mode 100644 README.md delete mode 100644 application.py delete mode 100644 gunicorn.conf.py delete mode 100644 requirements.txt delete mode 100644 rules.md delete mode 100644 static/behavior.js delete mode 100644 static/logo.svg delete mode 100644 static/manifest.json delete mode 100644 static/service_worker.js delete mode 100644 static/stylesheet.css delete mode 100644 templates/administrator.html delete mode 100644 templates/game.html delete mode 100644 templates/layout.html delete mode 100644 templates/login.html diff --git a/README.md b/README.md deleted file mode 100644 index 6109ea3..0000000 --- a/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# SCOicide SquadⒷ - - - -> A python Flask based role play game app đŸ”Ș - -→ [Live Page](https://example.com/) - -→ [Game rules](rules.md) \ No newline at end of file diff --git a/application.py b/application.py deleted file mode 100644 index dafe69d..0000000 --- a/application.py +++ /dev/null @@ -1,653 +0,0 @@ -from gevent import monkey -monkey.patch_all() - -import sqlite3 -import string -import secrets -import io -import zoneinfo -import os -import markdown -from datetime import datetime, timezone -from passlib.hash import argon2 -import pyotp -import qrcode -import qrcode.image.svg -from apscheduler.schedulers.gevent import GeventScheduler -from apscheduler.triggers.cron import CronTrigger -from apscheduler.triggers.date import DateTrigger -from flask import Flask, request, session, redirect, render_template, url_for, abort, send_from_directory -from flask_socketio import SocketIO, emit, join_room - -application = Flask(__name__) -app = application # Fallback-Alias fĂŒr Gunicorn und Pella -application.secret_key = secrets.token_hex(64) -websocket_manager = SocketIO(application, manage_session=False, async_mode='gevent') -scheduler_instance = GeventScheduler(timezone="Europe/Berlin") -european_timezone = zoneinfo.ZoneInfo("Europe/Berlin") - -IDENTIFIER_LENGTH = 5 - -def get_database_connection(): - connection = sqlite3.connect("game_database.sqlite") - connection.row_factory = sqlite3.Row - return connection - -def initialize_database(): - connection = get_database_connection() - connection.execute(""" - CREATE TABLE IF NOT EXISTS game_configuration ( - configuration_key TEXT PRIMARY KEY, - configuration_value TEXT - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS player ( - player_identifier TEXT PRIMARY KEY, - password_hash TEXT, - display_name TEXT, - is_alive INTEGER DEFAULT 1, - score INTEGER DEFAULT 0, - target_identifier TEXT, - reset_token TEXT, - pending_attacker_identifier TEXT, - is_approved INTEGER DEFAULT 0, - pending_display_name TEXT, - late_joiner INTEGER DEFAULT 0 - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS administrator ( - administrator_identifier INTEGER PRIMARY KEY, - password_hash TEXT, - two_factor_secret TEXT, - two_factor_enabled INTEGER DEFAULT 0 - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS kill_log ( - kill_id INTEGER PRIMARY KEY AUTOINCREMENT, - killer_identifier TEXT, - victim_identifier TEXT, - victim_previous_target TEXT, - kill_timestamp TEXT - ) - """) - try: - connection.execute("ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN pending_display_name TEXT") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - cursor = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'") - if not cursor.fetchone(): - connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('game_status', 'pending')") - connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('start_time', '')") - connection.commit() - connection.close() - -def require_player_authentication(function): - def wrapper(*arguments, **keyword_arguments): - if "player_identifier" not in session: - return redirect(url_for("handle_login")) - return function(*arguments, **keyword_arguments) - wrapper.__name__ = function.__name__ - return wrapper - -def require_administrator_authentication(function): - def wrapper(*arguments, **keyword_arguments): - if not session.get("administrator_authenticated"): - return redirect(url_for("handle_administrator")) - return function(*arguments, **keyword_arguments) - wrapper.__name__ = function.__name__ - return wrapper - -def get_markdown_rules(): - if os.path.exists("rules.md"): - with open("rules.md", "r", encoding="utf-8") as markdown_file: - return markdown.markdown(markdown_file.read()) - return "

Regelwerk nicht gefunden

" - -def shuffle_player_targets(): - connection = get_database_connection() - players = connection.execute("SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall() - if len(players) > 1: - identifiers = [player_record["player_identifier"] for player_record in players] - secure_random_generator = secrets.SystemRandom() - secure_random_generator.shuffle(identifiers) - for index, identifier in enumerate(identifiers): - target = identifiers[(index + 1) % len(identifiers)] - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target, identifier)) - connection.commit() - connection.close() - websocket_manager.emit("game_state_changed") - -def process_administrator_action(action_type, form_data): - connection = get_database_connection() - has_game_state_changed = False - - if action_type == "start_game": - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone() - if alive_count_record["alive_count"] >= 2: - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE game_configuration SET configuration_value = ? WHERE configuration_key = 'start_time'", (datetime.now(european_timezone).isoformat(),)) - connection.commit() - connection.close() - shuffle_player_targets() - websocket_manager.emit("admin_state_changed", to="administrator_room") - return - - elif action_type == "stop_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "pause_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'paused' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "resume_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "reset_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'pending' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE game_configuration SET configuration_value = '' WHERE configuration_key = 'start_time'") - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, score = 0, target_identifier = NULL, pending_attacker_identifier = NULL") - connection.execute("DELETE FROM kill_log") - connection.commit() - has_game_state_changed = True - - elif action_type == "create_player": - display_name = form_data.get("display_name") - new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) - temporary_password = secrets.token_urlsafe(16) - hashed_password = argon2.hash(temporary_password) - connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name)) - connection.commit() - - elif action_type == "approve_player": - target_identifier = form_data.get("player_identifier") - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - is_late_joiner = 1 if status_record["configuration_value"] in ['running', 'paused'] else 0 - is_alive_status = 0 if is_late_joiner else 1 - connection.execute("UPDATE player SET is_approved = 1, is_alive = ?, late_joiner = ? WHERE player_identifier = ?", (is_alive_status, is_late_joiner, target_identifier)) - connection.commit() - has_game_state_changed = True - - elif action_type == "edit_name": - target_identifier = form_data.get("player_identifier") - new_name = form_data.get("new_display_name") - connection.execute("UPDATE player SET display_name = ? WHERE player_identifier = ?", (new_name, target_identifier)) - connection.commit() - has_game_state_changed = True - - elif action_type == "approve_name_change": - target_identifier = form_data.get("player_identifier") - connection.execute("UPDATE player SET display_name = pending_display_name, pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - has_game_state_changed = True - - elif action_type == "reject_name_change": - target_identifier = form_data.get("player_identifier") - connection.execute("UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - websocket_manager.emit("game_state_changed", to=target_identifier) - - elif action_type == "delete_player": - target_identifier = form_data.get("player_identifier") - connection.execute("DELETE FROM player WHERE player_identifier = ?", (target_identifier,)) - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - if status_record["configuration_value"] == 'running': - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - if alive_count_record["alive_count"] == 1: - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - connection.commit() - has_game_state_changed = True - - elif action_type == "revive_player": - target_identifier = form_data.get("player_identifier") - player_to_revive = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone() - if player_to_revive and player_to_revive["is_alive"] == 0: - alive_players = connection.execute("SELECT * FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall() - if len(alive_players) > 0: - random_living_player = secrets.choice(alive_players) - random_living_identifier = random_living_player["player_identifier"] - attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (random_living_identifier,)).fetchone() - if attacker: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target_identifier, attacker["player_identifier"])) - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = ? WHERE player_identifier = ?", (random_living_identifier, target_identifier)) - else: - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - has_game_state_changed = True - - elif action_type == "eliminate_player": - target_identifier = form_data.get("player_identifier") - victim = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone() - if victim and victim["is_alive"] == 1: - attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_identifier,)).fetchone() - new_target = victim["target_identifier"] - connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (target_identifier,)) - - attacker_id = None - if attacker: - attacker_id = attacker["player_identifier"] - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target, attacker_id)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp) VALUES (?, ?, ?, ?)", (attacker_id, target_identifier, new_target, timestamp)) - - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - current_status = status_record["configuration_value"] if status_record else "unknown" - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - - if alive_count_record["alive_count"] == 1 and current_status != 'finished': - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - - connection.commit() - has_game_state_changed = True - - elif action_type == "edit_kill": - kill_id = form_data.get("kill_id") - new_killer_id = form_data.get("new_killer_identifier") - if new_killer_id == "": - new_killer_id = None - new_timestamp = form_data.get("kill_timestamp") - - old_kill = connection.execute("SELECT * FROM kill_log WHERE kill_id = ?", (kill_id,)).fetchone() - if old_kill: - old_killer_id = old_kill["killer_identifier"] - if old_killer_id != new_killer_id: - if old_killer_id: - connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (old_killer_id,)) - if new_killer_id: - connection.execute("UPDATE player SET score = score + 1 WHERE player_identifier = ?", (new_killer_id,)) - - connection.execute("UPDATE kill_log SET killer_identifier = ?, kill_timestamp = ? WHERE kill_id = ?", (new_killer_id, new_timestamp, kill_id)) - connection.commit() - has_game_state_changed = True - - elif action_type == "revoke_kill": - kill_id = form_data.get("kill_id") - kill_record = connection.execute("SELECT * FROM kill_log WHERE kill_id = ?", (kill_id,)).fetchone() - - if kill_record: - if kill_record["killer_identifier"]: - connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (kill_record["killer_identifier"],)) - - victim_id = kill_record["victim_identifier"] - target_to_restore = kill_record["victim_previous_target"] - - player_pointing_to_target = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_to_restore,)).fetchone() - - connection.execute("UPDATE player SET is_alive = 1, target_identifier = ? WHERE player_identifier = ?", (target_to_restore, victim_id)) - if player_pointing_to_target: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (victim_id, player_pointing_to_target["player_identifier"])) - - connection.execute("DELETE FROM kill_log WHERE kill_id = ?", (kill_id,)) - - alive_count = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()["alive_count"] - if alive_count > 1: - status = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()["configuration_value"] - if status == 'finished': - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE player SET score = MAX(0, score - 3) WHERE is_alive = 1 AND is_approved = 1 AND player_identifier != ?", (victim_id,)) - - connection.commit() - has_game_state_changed = True - - elif action_type == "change_target": - player_identifier = form_data.get("player_identifier") - new_target_identifier = form_data.get("new_target_identifier") - if player_identifier and new_target_identifier and player_identifier != new_target_identifier: - player_record = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (player_identifier,)).fetchone() - other_player_record = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (new_target_identifier,)).fetchone() - if player_record and other_player_record: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target_identifier, player_identifier)) - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (player_record["target_identifier"], other_player_record["player_identifier"])) - connection.commit() - has_game_state_changed = True - - elif action_type == "generate_reset": - target_identifier = form_data.get("player_identifier") - reset_token = secrets.token_urlsafe(32) - connection.execute("UPDATE player SET reset_token = ? WHERE player_identifier = ?", (reset_token, target_identifier)) - connection.commit() - - elif action_type == "update_credentials": - new_password = form_data.get("password_string") - password_confirmation = form_data.get("password_string_confirmation") - if new_password and new_password == password_confirmation: - hashed_password = argon2.hash(new_password) - connection.execute("UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1", (hashed_password,)) - connection.commit() - - elif action_type == "enable_two_factor": - two_factor_input = form_data.get("two_factor_code") - administrator_record = connection.execute("SELECT two_factor_secret FROM administrator WHERE administrator_identifier = 1").fetchone() - time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"]) - if time_based_one_time_password_validator.verify(two_factor_input): - connection.execute("UPDATE administrator SET two_factor_enabled = 1 WHERE administrator_identifier = 1") - connection.commit() - - elif action_type == "disable_two_factor": - connection.execute("UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1") - connection.commit() - - connection.close() - - if has_game_state_changed: - websocket_manager.emit("game_state_changed") - else: - websocket_manager.emit("admin_state_changed", to="administrator_room") - -def scheduled_action_wrapper(action_type, form_data): - with application.app_context(): - process_administrator_action(action_type, form_data) - -@application.route("/service_worker.js") -def serve_service_worker(): - return send_from_directory(application.static_folder, "service_worker.js") - -@application.route("/health") -@application.route("/ping") -def handle_health_check(): - return "OK", 200 - -@application.route("/") -@require_player_authentication -def handle_index(): - connection = get_database_connection() - player = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (session["player_identifier"],)).fetchone() - if not player: - session.clear() - return redirect(url_for("handle_login")) - status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - game_status = status_row["configuration_value"] - time_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'start_time'").fetchone() - start_time = time_row["configuration_value"] - - target_player = None - if player["target_identifier"]: - target_player = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["target_identifier"],)).fetchone() - - pending_attacker_name = None - if player["pending_attacker_identifier"]: - attacker = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["pending_attacker_identifier"],)).fetchone() - if attacker: - pending_attacker_name = attacker["display_name"] - - all_players = connection.execute("SELECT * FROM player WHERE is_approved = 1 ORDER BY score DESC").fetchall() - total_eliminations = sum(player_record["score"] for player_record in all_players) - alive_count = sum(1 for player_record in all_players if player_record["is_alive"] == 1) - connection.close() - - document_markup = get_markdown_rules() - - return render_template( - "game.html", - player=player, - game_status=game_status, - start_time=start_time, - target_player=target_player, - pending_attacker_name=pending_attacker_name, - all_players=all_players, - total_eliminations=total_eliminations, - alive_count=alive_count, - rules_markup=document_markup - ) - -@application.route("/register", methods=["GET", "POST"]) -def handle_registration(): - if request.method == "POST": - display_name = request.form.get("display_name") - password_input = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if password_input and password_input == password_confirmation: - new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) - hashed_password = argon2.hash(password_input) - connection = get_database_connection() - connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name)) - connection.commit() - connection.close() - websocket_manager.emit("admin_state_changed", to="administrator_room") - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, registration_success=True, new_identifier=new_identifier) - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, register_mode=True) - -@application.route("/login", methods=["GET", "POST"]) -def handle_login(): - if request.method == "POST": - identifier_input = request.form.get("player_identifier", "").strip().upper() - password_input = request.form.get("password_string", "") - connection = get_database_connection() - player = connection.execute("SELECT password_hash FROM player WHERE player_identifier = ?", (identifier_input,)).fetchone() - connection.close() - if player and argon2.verify(password_input, player["password_hash"]): - session["player_identifier"] = identifier_input - return redirect(url_for("handle_index")) - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH) - -@application.route("/reset_password", methods=["GET", "POST"]) -def handle_password_reset(): - token = request.args.get("token") or request.form.get("token") - if not token: - abort(400) - connection = get_database_connection() - player = connection.execute("SELECT player_identifier FROM player WHERE reset_token = ?", (token,)).fetchone() - if not player: - connection.close() - abort(403) - if request.method == "POST": - new_password = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if new_password and new_password == password_confirmation: - hashed_password = argon2.hash(new_password) - connection.execute("UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?", (hashed_password, token)) - connection.commit() - connection.close() - return redirect(url_for("handle_login")) - connection.close() - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token) - -@application.route("/player/action", methods=["POST"]) -@require_player_authentication -def handle_player_action(): - action_type = request.form.get("action_type") - connection = get_database_connection() - if action_type == "request_name_change": - pending_name = request.form.get("pending_display_name") - connection.execute("UPDATE player SET pending_display_name = ? WHERE player_identifier = ?", (pending_name, session["player_identifier"])) - connection.commit() - websocket_manager.emit("admin_state_changed", to="administrator_room") - connection.close() - return redirect(url_for("handle_index")) - -@application.route("/logout") -def handle_logout(): - session.clear() - return redirect(url_for("handle_login")) - -@application.route("/admin", methods=["GET", "POST"]) -def handle_administrator(): - connection = get_database_connection() - administrator_record = connection.execute("SELECT * FROM administrator WHERE administrator_identifier = 1").fetchone() - - if not administrator_record: - if request.method == "POST": - password_input = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if password_input and password_input == password_confirmation: - hashed_password = argon2.hash(password_input) - two_factor_secret = pyotp.random_base32() - connection.execute("INSERT INTO administrator (administrator_identifier, password_hash, two_factor_secret, two_factor_enabled) VALUES (1, ?, ?, 0)", (hashed_password, two_factor_secret)) - connection.commit() - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - connection.close() - return render_template("administrator.html", setup_required=True) - - if not session.get("administrator_authenticated"): - if request.method == "POST": - password_input = request.form.get("password_string") - if argon2.verify(password_input, administrator_record["password_hash"]): - if administrator_record["two_factor_enabled"]: - two_factor_input = request.form.get("two_factor_code") - time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"]) - if time_based_one_time_password_validator.verify(two_factor_input): - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - else: - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - connection.close() - return render_template("administrator.html", login_required=True, two_factor_enabled=bool(administrator_record["two_factor_enabled"])) - - status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - game_status = status_row["configuration_value"] - all_players = connection.execute("SELECT * FROM player ORDER BY is_approved ASC, display_name ASC").fetchall() - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone() - can_start = alive_count_record["alive_count"] >= 2 - - kill_logs = connection.execute(""" - SELECT k.*, - killer.display_name as killer_name, - victim.display_name as victim_name - FROM kill_log k - LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier - LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier - ORDER BY k.kill_timestamp DESC - """).fetchall() - - two_factor_uri = pyotp.totp.TOTP(administrator_record["two_factor_secret"]).provisioning_uri(name="Administrator", issuer_name="AssassinGame") - qr_factory = qrcode.image.svg.SvgPathImage - qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory) - qr_stream = io.BytesIO() - qr_image.save(qr_stream) - qr_code_svg = qr_stream.getvalue().decode("utf-8") - - connection.close() - - return render_template( - "administrator.html", - dashboard_active=True, - game_status=game_status, - can_start=can_start, - all_players=all_players, - kill_logs=kill_logs, - two_factor_secret=administrator_record["two_factor_secret"], - two_factor_enabled=bool(administrator_record["two_factor_enabled"]), - qr_code_svg=qr_code_svg - ) - -@application.route("/admin/action", methods=["POST"]) -@require_administrator_authentication -def handle_administrator_action(): - form_data = request.form.to_dict() - action_type = form_data.get("action_type") - execution_time_string = form_data.get("execution_time") - - if execution_time_string: - execution_datetime = datetime.strptime(execution_time_string[:16], "%Y-%m-%dT%H:%M").replace(tzinfo=european_timezone) - scheduler_instance.add_job(scheduled_action_wrapper, DateTrigger(run_date=execution_datetime), args=[action_type, form_data]) - else: - process_administrator_action(action_type, form_data) - - return redirect(url_for("handle_administrator")) - -@websocket_manager.on("connect") -def handle_websocket_connection(): - if "player_identifier" in session: - join_room(session["player_identifier"]) - if session.get("administrator_authenticated"): - join_room("administrator_room") - -@websocket_manager.on("request_elimination") -def handle_elimination_request(): - attacker_identifier = session.get("player_identifier") - if not attacker_identifier: - return - connection = get_database_connection() - attacker = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (attacker_identifier,)).fetchone() - if attacker and attacker["target_identifier"]: - victim_identifier = attacker["target_identifier"] - connection.execute("UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?", (attacker_identifier, victim_identifier)) - connection.commit() - emit("elimination_verification_requested", to=victim_identifier) - connection.close() - -@websocket_manager.on("confirm_elimination") -def handle_elimination_confirmation(): - victim_identifier = session.get("player_identifier") - if not victim_identifier: - return - connection = get_database_connection() - victim = connection.execute("SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - if victim and victim["pending_attacker_identifier"]: - attacker_identifier = victim["pending_attacker_identifier"] - new_target = victim["target_identifier"] - - connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,)) - connection.execute("UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?", (new_target, attacker_identifier)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp) VALUES (?, ?, ?, ?)", (attacker_identifier, victim_identifier, new_target, timestamp)) - - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - current_status = status_record["configuration_value"] if status_record else "unknown" - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - - if alive_count_record["alive_count"] == 1 and current_status != 'finished': - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - - connection.commit() - websocket_manager.emit("game_state_changed") - connection.close() - -@websocket_manager.on("reject_elimination") -def handle_elimination_rejection(): - victim_identifier = session.get("player_identifier") - if not victim_identifier: - return - connection = get_database_connection() - victim = connection.execute("SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - if victim and victim["pending_attacker_identifier"]: - attacker_identifier = victim["pending_attacker_identifier"] - connection.execute("UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,)) - connection.commit() - emit("elimination_rejected", to=attacker_identifier) - connection.close() - -# --- PRODUKTIONS-SETUP (GUNICORN) --- -with application.app_context(): - initialize_database() - -try: - if getattr(scheduler_instance, 'state', 0) == 0: - scheduler_instance.start() -except Exception: - pass - -if __name__ == "__main__": - websocket_manager.run(application, host="0.0.0.0", port=5000) diff --git a/gunicorn.conf.py b/gunicorn.conf.py deleted file mode 100644 index 89be1ed..0000000 --- a/gunicorn.conf.py +++ /dev/null @@ -1,2 +0,0 @@ -worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -workers = 1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a76fd41..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -Flask -Flask-SocketIO -APScheduler -passlib -argon2-cffi -pyotp -qrcode -Markdown -gunicorn -gevent -gevent-websocket diff --git a/rules.md b/rules.md deleted file mode 100644 index f7eb5ff..0000000 --- a/rules.md +++ /dev/null @@ -1,91 +0,0 @@ -# *SCO*icide Squad -_Ein Spiel am Science College Overbach_ - -## Dein Ziel -_Du hast immer genau eine Person zu erwischen._ - -- Auf deinem Dashboard steht dein aktuelles Ziel. Es ist immer genau eine Person. - -- Gleichzeitig jagt dich jemand. Wer das ist, erfĂ€hrst du erst, wenn es passiert. - -- Erwischst du dein Ziel, ĂŒbernimmst du automatisch dessen Ziel. Die Kette bleibt geschlossen. - -- Über dein Ziel wird nicht geredet. Wer sein Ziel verrĂ€t oder gemeinsam mit anderen Ziele abgleicht, nimmt allen den Spaß. - -## Wie ein Kill zĂ€hlt -_Übergeben eines Gegenstandes, direkt und ungesehen_ - -- Du killst dein Ziel, indem du ihm einen beliebigen Gegenstand ĂŒbergibst. - -- Das Ziel muss den Gegenstand direkt in die Hand gegeben bekommen. Unvorbereitetes Abwerfen gilt nicht. Im Zweifel gilt sonst der Kill nicht. Achtet auf Fairness! - -- Sind andere lebende Personen unmittelbar dabei und sehen den Mord, zĂ€hlt der Versuch nicht. - -- Wehrt sich dein Ziel erfolgreich, hast du Pech gehabt. Ein weiterer Versuch ist jederzeit erlaubt. - -## Melden -_Der TĂ€ter meldet, das Opfer bestĂ€tigt._ - -- Wer sein Ziel erwischt, meldet das selbst unter dem Reiter „Ziel“ in der App. - -- Die Meldung wird geprĂŒft: Das Ziel bestĂ€tigt, ob du es erwischt hast. - -- Melde zĂŒgig: Bis zur Meldung lĂ€uft die Mordkette unverĂ€ndert weiter. - -- Stimmt etwas nicht, korrigiert die Spielleitung die Meldung. - -## Punkte -_Jeder Kill zĂ€hlt, gekillt werden kostet keine Punkte._ - -- FĂŒr jeden Kill gibt es einen Punkt. Deine Punkte bleiben dir auch, wenn du gekillt wirst. - -- Die letzte ĂŒberlebende Person bekommt zusĂ€tzlich drei Punkte fĂŒr's Überleben. - -- Der Punktestand ist fĂŒr alle einsehbar und aktualisiert sich nach jeder Meldung. - -## GeschĂŒtzte Orte und Zeiten -_Es gibt Bereiche, in denen nicht gespielt wird._ - -- GeschĂŒtzt sind nur Zimmernachbarn in ihren jeweiligen Zimmern. Dein Zimmernachbar darf dich nicht umbringen, wenn ihr alleine im Zimmer seid. - -- Wer krank ist oder eine Pause braucht, sagt bescheid. Das wird respektiert. - -## Was nicht geht -_Keine Gewalt, kein Zwang, kein fremdes Eigentum_ - -- Kein körperliche Gewalt und kein Festhalten. - -- Achtet fremdes Eigentum und die PrivatsphĂ€re anderer. - -- Nichts beschĂ€digen und niemanden in eine unangenehme Lage bringen. - -- Ein Nein ist ein Nein, auch mitten im Spiel. - -## Ablauf -_Das Spiel lĂ€uft ĂŒber mehrere Tage bis nur noch eine Person ĂŒbrig ist._ - -- Das Spiel startet, sobald die Spielleitung auslost. - -- Die Spielleitung kann jederzeit pausieren. Dann sind keine Eliminierungen gĂŒltig. - -- Das Spiel endet, wenn nur noch eine Person ĂŒbrig ist oder die Spielleitung es beendet. - -## Dein Zugang -_FĂŒnfstellige ID und dein Passwort bringen dich immer zurĂŒck._ - -- Deine ID steht oben. Sie Ă€ndert sich nie. - -- Mit ID und Passwort kommst du auf jedem GerĂ€t wieder in deinen Account. - -- Auf diesem GerĂ€t bleibst du angemeldet, bist du dich abmeldest. - -- Passwort vergessen? Die Spielleitung kann dir einen Link zum ZurĂŒcksetzen geben. - -## Spielleitung -_Bei Streit entscheidet die Spielleitung._ - -- Die Spielleitung sieht die vollstĂ€ndige Kette und alle Meldungen. - -- Die Spielleitung kann ZugĂ€nge und das Spiel verwalten. - -- Bei Unklarheiten entscheidet die Spielleitung. \ No newline at end of file diff --git a/static/behavior.js b/static/behavior.js deleted file mode 100644 index 36ccec9..0000000 --- a/static/behavior.js +++ /dev/null @@ -1,335 +0,0 @@ -let websocket_connection; -let pending_administrator_form = null; -let stopwatch_interval_identifier = null; - -function initialize_lucide_icons() { - if (typeof lucide !== "undefined") { - lucide.createIcons(); - } -} - -function update_game_stopwatch() { - const stopwatch_element = document.getElementById("game-stopwatch"); - if (!stopwatch_element) { - return; - } - - const start_time_string = stopwatch_element.getAttribute("data-start-time"); - if (!start_time_string) { - return; - } - - const start_date = new Date(start_time_string); - if (isNaN(start_date.getTime())) { - return; - } - - const current_date = new Date(); - const difference_in_milliseconds = current_date.getTime() - start_date.getTime(); - - if (difference_in_milliseconds < 0) { - stopwatch_element.textContent = "00:00 Minuten"; - return; - } - - const total_seconds = Math.floor(difference_in_milliseconds / 1000); - const days_passed = Math.floor(total_seconds / 86400); - const hours_passed = Math.floor((total_seconds % 86400) / 3600); - const minutes_passed = Math.floor((total_seconds % 3600) / 60); - const seconds_passed = total_seconds % 60; - - let output_string = ""; - - if (days_passed === 1) { - output_string += "1 Tag und "; - } else if (days_passed > 1) { - output_string += days_passed + " Tage und "; - } - - const padded_minutes = String(minutes_passed).padStart(2, "0"); - const padded_seconds = String(seconds_passed).padStart(2, "0"); - - if (hours_passed === 1) { - output_string += "1:" + padded_minutes + ":" + padded_seconds + " Stunde"; - } else if (hours_passed > 1) { - output_string += hours_passed + ":" + padded_minutes + ":" + padded_seconds + " Stunden"; - } else { - output_string += padded_minutes + ":" + padded_seconds + " Minuten"; - } - - stopwatch_element.textContent = output_string; -} - -function initialize_application_behavior() { - initialize_lucide_icons(); - - if (stopwatch_interval_identifier) { - clearInterval(stopwatch_interval_identifier); - } - stopwatch_interval_identifier = setInterval(update_game_stopwatch, 1000); - update_game_stopwatch(); - - const tab_buttons = document.querySelectorAll(".tab-button"); - tab_buttons.forEach(function(current_button) { - current_button.addEventListener("click", function() { - const target_identifier = current_button.getAttribute("data-target"); - const parent_container = current_button.closest(".application-content"); - - parent_container.querySelectorAll(".tab-button").forEach(function(button_element) { - button_element.classList.remove("active-tab"); - }); - parent_container.querySelectorAll(".tab-content").forEach(function(content_element) { - content_element.classList.remove("active-content"); - }); - - current_button.classList.add("active-tab"); - const target_panel = document.getElementById(target_identifier); - if (target_panel) { - target_panel.classList.add("active-content"); - } - }); - }); - - const player_forms = document.querySelectorAll(".asynchronous-form"); - player_forms.forEach(function(current_form) { - current_form.addEventListener("submit", function(form_submission_event) { - form_submission_event.preventDefault(); - execute_form_submission(current_form); - }); - }); - - const administrator_forms = document.querySelectorAll(".admin-asynchronous-form"); - administrator_forms.forEach(function(current_form) { - current_form.addEventListener("submit", function(form_submission_event) { - form_submission_event.preventDefault(); - pending_administrator_form = current_form; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - const time_input = document.getElementById("modal_execution_time"); - if (time_input) { - time_input.value = ""; - } - administrator_modal.classList.add("active-modal"); - } - }); - }); - - const confirm_administrator_button = document.getElementById("confirm-admin-action"); - if (confirm_administrator_button) { - confirm_administrator_button.addEventListener("click", function() { - if (pending_administrator_form) { - const time_input = document.getElementById("modal_execution_time"); - if (time_input && time_input.value) { - let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); - if (!hidden_time_input) { - hidden_time_input = document.createElement("input"); - hidden_time_input.type = "hidden"; - hidden_time_input.name = "execution_time"; - pending_administrator_form.appendChild(hidden_time_input); - } - hidden_time_input.value = time_input.value; - } else { - let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); - if (hidden_time_input) { - hidden_time_input.remove(); - } - } - execute_form_submission(pending_administrator_form); - pending_administrator_form = null; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - administrator_modal.classList.remove("active-modal"); - } - } - }); - } - - const cancel_administrator_button = document.getElementById("cancel-admin-action"); - if (cancel_administrator_button) { - cancel_administrator_button.addEventListener("click", function() { - pending_administrator_form = null; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - administrator_modal.classList.remove("active-modal"); - } - }); - } - - const trigger_elimination_button = document.getElementById("trigger-elimination"); - if (trigger_elimination_button) { - trigger_elimination_button.addEventListener("click", function() { - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.add("active-modal"); - } - }); - } - - const proceed_elimination_button = document.getElementById("proceed-elimination"); - if (proceed_elimination_button) { - proceed_elimination_button.addEventListener("click", function() { - websocket_connection.emit("request_elimination"); - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.remove("active-modal"); - } - const waiting_modal = document.getElementById("waiting-modal"); - if (waiting_modal) { - waiting_modal.classList.add("active-modal"); - } - }); - } - - const cancel_elimination_button = document.getElementById("cancel-elimination"); - if (cancel_elimination_button) { - cancel_elimination_button.addEventListener("click", function() { - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.remove("active-modal"); - } - }); - } - - const confirm_button = document.getElementById("confirm-elimination"); - if (confirm_button) { - confirm_button.addEventListener("click", function() { - websocket_connection.emit("confirm_elimination"); - const modal_element = document.getElementById("verification-modal"); - if (modal_element) { - modal_element.classList.remove("active-modal"); - } - }); - } - - const reject_button = document.getElementById("reject-elimination"); - if (reject_button) { - reject_button.addEventListener("click", function() { - websocket_connection.emit("reject_elimination"); - const modal_element = document.getElementById("verification-modal"); - if (modal_element) { - modal_element.classList.remove("active-modal"); - } - }); - } - - const acknowledge_rejection_button = document.getElementById("acknowledge-rejection"); - if (acknowledge_rejection_button) { - acknowledge_rejection_button.addEventListener("click", function() { - const rejection_modal = document.getElementById("rejection-modal"); - if (rejection_modal) { - rejection_modal.classList.remove("active-modal"); - } - }); - } - - const open_name_modal_button = document.getElementById("trigger-name-change-modal"); - if (open_name_modal_button) { - open_name_modal_button.addEventListener("click", function() { - const name_modal = document.getElementById("name-change-modal"); - if (name_modal) { - name_modal.classList.add("active-modal"); - } - }); - } - - const close_name_modal_button = document.getElementById("cancel-name-change"); - if (close_name_modal_button) { - close_name_modal_button.addEventListener("click", function() { - const name_modal = document.getElementById("name-change-modal"); - if (name_modal) { - name_modal.classList.remove("active-modal"); - } - }); - } -} - -function execute_form_submission(target_form) { - const active_tab_element = document.querySelector(".active-tab"); - let active_target = "control-panel"; - if (active_tab_element) { - active_target = active_tab_element.getAttribute("data-target"); - } - - fetch(target_form.action, { - method: target_form.method, - body: new FormData(target_form) - }) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - const new_tab_button = document.querySelector(".tab-button[data-target='" + active_target + "']"); - if (new_tab_button) { - new_tab_button.click(); - } - }); -} - -function replace_main_content(markup_text) { - const document_parser = new DOMParser(); - const parsed_document = document_parser.parseFromString(markup_text, "text/html"); - const current_container = document.querySelector(".application-content"); - const new_container = parsed_document.querySelector(".application-content"); - if (current_container && new_container) { - current_container.innerHTML = new_container.innerHTML; - initialize_application_behavior(); - } -} - -document.addEventListener("DOMContentLoaded", function() { - if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("/service_worker.js"); - } - - if (typeof io !== "undefined") { - websocket_connection = io(); - - websocket_connection.on("game_state_changed", function() { - if (document.querySelector(".game-section") || document.querySelector(".administrator-section")) { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - }); - } - }); - - websocket_connection.on("admin_state_changed", function() { - if (document.querySelector(".administrator-section")) { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - }); - } - }); - - websocket_connection.on("elimination_verification_requested", function() { - const modal_element = document.getElementById("verification-modal"); - if (modal_element) { - modal_element.classList.add("active-modal"); - } else { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - const new_modal_element = document.getElementById("verification-modal"); - if (new_modal_element) { - new_modal_element.classList.add("active-modal"); - } - }); - } - }); - - websocket_connection.on("elimination_rejected", function() { - const waiting_modal = document.getElementById("waiting-modal"); - if (waiting_modal) { - waiting_modal.classList.remove("active-modal"); - } - const rejection_modal = document.getElementById("rejection-modal"); - if (rejection_modal) { - rejection_modal.classList.add("active-modal"); - } - }); - } - initialize_application_behavior(); -}); \ No newline at end of file diff --git a/static/logo.svg b/static/logo.svg deleted file mode 100644 index b2f2548..0000000 --- a/static/logo.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/static/manifest.json b/static/manifest.json deleted file mode 100644 index eeff66f..0000000 --- a/static/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "SCOicide Squad", - "short_name": "SCOicide", - "start_url": "/", - "display": "standalone", - "background_color": "#f0f2f5", - "theme_color": "#1a1e29", - "icons": [ - { - "src": "/static/logo.svg", - "sizes": "any", - "type": "image/svg+xml" - } - ] -} \ No newline at end of file diff --git a/static/service_worker.js b/static/service_worker.js deleted file mode 100644 index ccef124..0000000 --- a/static/service_worker.js +++ /dev/null @@ -1,23 +0,0 @@ -const cache_identifier = "scoicide-squad-cache-version-1"; - -self.addEventListener("install", function(installation_event) { - installation_event.waitUntil( - caches.open(cache_identifier).then(function(cache_object) { - return cache_object.addAll([ - "/", - "/static/stylesheet.css", - "/static/behavior.js", - "/static/logo.svg", - "/static/manifest.json" - ]); - }) - ); -}); - -self.addEventListener("fetch", function(network_fetch_event) { - network_fetch_event.respondWith( - caches.match(network_fetch_event.request).then(function(cached_response) { - return cached_response || fetch(network_fetch_event.request); - }) - ); -}); \ No newline at end of file diff --git a/static/stylesheet.css b/static/stylesheet.css deleted file mode 100644 index e66e0df..0000000 --- a/static/stylesheet.css +++ /dev/null @@ -1,443 +0,0 @@ -:root { - --lightness-primary: 0.35; - --chroma-primary: 0.15; - --hue-primary: 15; - - --lightness-background: 0.9; - --chroma-background: 0.01; - --hue-background: var(--hue-primary); - - --lightness-surface: 1; - --chroma-surface: 0; - --hue-surface: 0; - - --lightness-text: 0.15; - --chroma-text: 0.05; - --hue-text: 250; - - --lightness-success: 0.60; - --chroma-success: 0.18; - --hue-success: 140; - - --lightness-danger: 0.50; - --chroma-danger: 0.22; - --hue-danger: 25; - - --lightness-warning: 0.75; - --chroma-warning: 0.18; - --hue-warning: 70; - - --dimension-multiplier: 1; - --spacing-small: calc(0.5rem * var(--dimension-multiplier)); - --spacing-medium: calc(1rem * var(--dimension-multiplier)); - --spacing-large: calc(1.5rem * var(--dimension-multiplier)); - - --border-radius-small: calc(0.5rem * var(--dimension-multiplier)); - --border-radius-large: calc(1.25rem * var(--dimension-multiplier)); - --border-radius-pill: calc(999rem * var(--dimension-multiplier)); -} - -@media (prefers-color-scheme: dark) { - :root { - --lightness-background: 0.2; - --chroma-background: 0.02; - - --lightness-surface: 0.18; - --chroma-surface: 0.02; - - --lightness-text: 0.90; - --chroma-text: 0.02; - - --lightness-primary: 0.65; - --chroma-primary: 0.20; - } -} - -@media (prefers-reduced-motion: reduce) { - * { - transition-duration: 0s !important; - animation-duration: 0s !important; - } -} - -body { - background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); - color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); - font-family: system-ui, -apple-system, sans-serif; - font-size: calc(1rem * var(--dimension-multiplier)); - line-height: 1.6; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.application-header { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - color: oklch(1 0 0); - padding: var(--spacing-medium) var(--spacing-large); - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - position: sticky; - top: 0; - z-index: 100; -} - -.brand-container { - display: flex; - align-items: center; - gap: var(--spacing-small); -} - -.brand-logo { - width: calc(2rem * var(--dimension-multiplier)); - height: calc(2rem * var(--dimension-multiplier)); - color: currentColor; -} - -.application-title { - margin: 0; - font-size: calc(1.25rem * var(--dimension-multiplier)); - font-weight: 800; - letter-spacing: -0.02em; -} - -.application-content { - flex: 1; - padding: var(--spacing-large); - width: 100%; - max-width: calc(48rem * var(--dimension-multiplier)); - margin: 0 auto; - box-sizing: border-box; -} - -.content-card { - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - padding: var(--spacing-large); - border-radius: var(--border-radius-large); - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01); - margin-bottom: var(--spacing-large); -} - -.section-title { - margin-top: 0; - margin-bottom: var(--spacing-medium); - display: flex; - align-items: center; - gap: var(--spacing-small); -} - -.card-subtitle { - margin-top: 0; - margin-bottom: var(--spacing-small); - opacity: 0.7; - text-transform: uppercase; - font-size: calc(0.875rem * var(--dimension-multiplier)); - letter-spacing: 0.05em; -} - -.input-form { - display: flex; - flex-direction: column; - gap: var(--spacing-medium); -} - -.inline-form { - display: inline-block; -} - -.flexible_form_container { - display: flex; - gap: calc(0.5rem * var(--dimension-multiplier)); - align-items: center; - flex-wrap: wrap; -} - -.form-group { - display: flex; - flex-direction: column; - gap: calc(0.25rem * var(--dimension-multiplier)); -} - -.input-label { - font-weight: bold; - font-size: calc(0.875rem * var(--dimension-multiplier)); -} - -.text-input { - padding: var(--spacing-medium); - border: 2px solid oklch(0.5 0 0 / 0.2); - border-radius: var(--border-radius-small); - background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); - color: inherit; - font-size: inherit; - transition: border-color 0.2s; -} - -.text-input:focus { - outline: none; - border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); -} - -.small-input { - width: calc(12rem * var(--dimension-multiplier)); - padding: var(--spacing-small); -} - -.time-input { - width: calc(13rem * var(--dimension-multiplier)); -} - -.action-button { - padding: var(--spacing-medium) var(--spacing-large); - border: none; - border-radius: var(--border-radius-pill); - font-size: inherit; - font-weight: bold; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-small); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - color: oklch(1 0 0); -} - -.action-button:hover, .action-button:focus { - filter: brightness(1.1); - transform: translateY(-1px); -} - -.icon-button { - padding: calc(0.5rem * var(--dimension-multiplier)); - border-radius: var(--border-radius-small); -} - -.primary-button { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3); -} - -.success-button { - background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); -} - -.danger-button { - background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); -} - -.warning-button { - background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); - color: oklch(0.2 0 0); -} - -.secondary-button { - background-color: transparent; - color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); - border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2); -} - -.large-button { - padding: var(--spacing-large) calc(2.5rem * var(--dimension-multiplier)); - font-size: calc(1.125rem * var(--dimension-multiplier)); - width: 100%; -} - -.small-button { - padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.75rem * var(--dimension-multiplier)); - font-size: calc(0.875rem * var(--dimension-multiplier)); -} - -.tab-container { - display: flex; - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - border-radius: var(--border-radius-pill); - padding: calc(0.375rem * var(--dimension-multiplier)); - gap: calc(0.25rem * var(--dimension-multiplier)); - box-shadow: 0 4px 12px rgba(0,0,0,0.05); - margin-bottom: var(--spacing-large); - overflow-x: auto; -} - -.tab-button { - flex: 1; - border-radius: var(--border-radius-pill); - padding: calc(0.625rem * var(--dimension-multiplier)) var(--spacing-medium); - border: none; - background: transparent; - color: inherit; - font-size: inherit; - font-weight: bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: calc(0.5rem * var(--dimension-multiplier)); - white-space: nowrap; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.active-tab { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - color: oklch(1 0 0); - box-shadow: 0 2px 8px rgba(0,0,0,0.15); -} - -.tab-content { - display: none; -} - -.active-content { - display: block; -} - -.status-pill { - display: inline-flex; - align-items: center; - gap: calc(0.375rem * var(--dimension-multiplier)); - padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.875rem * var(--dimension-multiplier)); - border-radius: var(--border-radius-pill); - font-size: calc(0.875rem * var(--dimension-multiplier)); - font-weight: bold; - color: oklch(1 0 0); - margin-left: var(--spacing-small); -} - -.warning-pill { - background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); - color: oklch(0.2 0 0); -} - -.success-pill { - background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); -} - -.danger-pill { - background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); -} - -.notice-card { - border-left: 6px solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); -} - -.danger-card { - border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); -} - -.success-card { - border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); -} - -.warning-card { - border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); -} - -.target-name { - font-size: calc(2.5rem * var(--dimension-multiplier)); - text-align: center; - margin: var(--spacing-large) 0; - color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - font-weight: 800; -} - -.data-table { - width: 100%; - border-collapse: collapse; -} - -.data-table th, .data-table td { - padding: var(--spacing-medium); - border-bottom: 1px solid oklch(0.5 0 0 / 0.1); - text-align: left; -} - -.data-table th { - font-weight: 800; - text-transform: uppercase; - font-size: calc(0.75rem * var(--dimension-multiplier)); - letter-spacing: 0.05em; - opacity: 0.7; -} - -.highlighted-row { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1); -} - -.highlighted-row td { - font-weight: bold; -} - -.unregistered-row { - opacity: 0.6; -} - -.markdown-container h1, .markdown-container h2, .markdown-container h3 { - color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); -} - -.markdown-container ul { - padding-left: var(--spacing-large); -} - -.modal-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.6); - backdrop-filter: blur(4px); - justify-content: center; - align-items: center; - z-index: 1000; -} - -.active-modal { - display: flex; -} - -.modal-dialog { - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - padding: var(--spacing-large); - border-radius: var(--border-radius-large); - max-width: calc(32rem * var(--dimension-multiplier)); - width: 90%; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); -} - -.modal-title { - margin-top: 0; - display: flex; - align-items: center; - gap: var(--spacing-small); -} - -.modal-actions { - display: flex; - gap: var(--spacing-medium); - justify-content: flex-end; - margin-top: var(--spacing-large); -} - -.qr-code-container { - background-color: oklch(1 0 0); - padding: var(--spacing-medium); - border-radius: var(--border-radius-large); - display: inline-block; - margin-bottom: var(--spacing-medium); - box-shadow: inset 0 0 0 2px oklch(0.5 0 0 / 0.1); -} - -.qr-code-container svg { - display: block; - width: calc(14rem * var(--dimension-multiplier)); - height: calc(14rem * var(--dimension-multiplier)); -} - -.monospace-identifier { - font-family: monospace; -} \ No newline at end of file diff --git a/templates/administrator.html b/templates/administrator.html deleted file mode 100644 index a6a9006..0000000 --- a/templates/administrator.html +++ /dev/null @@ -1,333 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -
- {% if setup_required %} -
-

Administrator Einrichtung

-
-
- - -
-
- - -
- -
-
- {% elif login_required %} -
-

Administrator Anmeldung

-
-
- - -
- {% if two_factor_enabled %} -
- - -
- {% endif %} - -
-
- {% elif dashboard_active %} -
- - - -
- -
-
-

- Spielstatus: - {% if game_status == 'pending' %} - Ausstehend - {% elif game_status == 'running' %} - LĂ€uft - {% elif game_status == 'paused' %} - Pausiert - {% elif game_status == 'finished' %} - Beendet - {% endif %} -

- - {% if game_status == 'pending' %} - {% if can_start %} -
- - -
- {% else %} -
-

Es werden mindestens zwei freigegebene Spieler benötigt, um das Spiel zu starten.

-
- {% endif %} - {% elif game_status == 'running' %} -
- - -
-
- - -
- {% elif game_status == 'paused' %} -
- - -
-
- - -
- {% elif game_status == 'finished' %} -
- - -
- {% endif %} -
- -
-

Sicherheit

- - {% if two_factor_enabled %} -
-

Zwei-Faktor-Authentifizierung ist aktiv.

-
- - -
-
- {% else %} -
-

Zwei-Faktor-Authentifizierung ist inaktiv. Bitte scanne den QR-Code und bestÀtige zur Aktivierung.

-

Manuelle Eingabe: {{ two_factor_secret }}

-
- {{ qr_code_svg | safe }} -
-
- -
- - -
- -
-
- {% endif %} - -
- -
- - -
-
- - -
- -
-
-
- -
-
-

Spieler manuell erstellen

-
- -
- - -
- -
-
- -
- - - - - - - - - - - - {% for current_player in all_players %} - - - - - - - - {% endfor %} - -
KennungName & ÄnderungStatusZielAktionen
- {{ current_player.player_identifier }} - {% if not current_player.is_approved %} -
Wartet - {% elif current_player.late_joiner %} -
SpÀteinstieg - {% endif %} -
-
- - - - -
- {% if current_player.pending_display_name %} -
-

- WĂŒnscht: {{ current_player.pending_display_name }} -

-
- - - -
-
- - - -
-
- {% endif %} -
- {% if not current_player.is_approved %} -
- - - -
- {% else %} - {% if current_player.is_alive %} - Lebend -
- - - -
- {% else %} - Tot -
- - - -
- {% endif %} - {% endif %} -
- {% if current_player.target_identifier %} - {{ current_player.target_identifier }} -
- - - - -
- {% else %} - - - {% endif %} -
-
- - - -
- {% if current_player.reset_token %} - - {% endif %} -
- - - -
-
-
-
- -
-
- - - - - - - - - - - {% for kill in kill_logs %} - - - - - - - {% else %} - - - - {% endfor %} - -
ZeitpunktKillerOpferAktionen
-
- - - -
- - - {{ kill.victim_name or 'Unbekannt' }} ({{ kill.victim_identifier }}) - - - - -
- - - -
-
Bisher keine Kills im System.
-
-
- - - {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/templates/game.html b/templates/game.html deleted file mode 100644 index 52283e1..0000000 --- a/templates/game.html +++ /dev/null @@ -1,187 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -
-
-

Willkommen, {{ player.display_name }} ({{ player.player_identifier }})!

-
- - {% if not player.is_approved %} -
- -
- {% elif game_status == 'pending' %} -
- -
- {% elif game_status == 'finished' %} -
-

Spiel beendet – Rangliste

- - - - - - - - - - {% for current_player in all_players %} - - - - - - {% endfor %} - -
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
-
- {% elif game_status in ['running', 'paused'] %} -
- - - -
- -
- {% if game_status == 'paused' %} -
- -

Das Spiel wurde pausiert. Eliminierungen sind aktuell nicht möglich.

-
- {% endif %} - - {% if player.is_alive %} -
-

Dein Ziel

-

{{ target_player.display_name if target_player else 'Niemand' }}

- {% if game_status == 'running' %} - - {% else %} - - {% endif %} -
- {% elif player.late_joiner %} -
- -

Du bist dem Spiel wÀhrend einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.

-
- {% else %} -
- -
- {% endif %} - -
-

SpielĂŒbersicht

-

Überlebende: {{ alive_count }}

-

Deine Kills: {{ player.score }}

-

Gesamte Kills: {{ total_eliminations }}

-

Laufzeit: Wird berechnet...

-
- -
-

Profil

- {% if player.pending_display_name %} -
-

Der Name {{ player.pending_display_name }} wird geprĂŒft…

-
- {% else %} - - {% endif %} -
-
- -
-
- - - - - - - - - - {% for current_player in all_players %} - - - - - - {% endfor %} - -
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
-
-
- -
-
- {{ rules_markup | safe }} -
-
- - - - - - - - - - - {% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html deleted file mode 100644 index a28e45f..0000000 --- a/templates/layout.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - SCOicide Squad - - - - - - - - - -
-
- -

SCOicide Squad

-
- -
-
- {% block content %}{% endblock %} -
- - \ No newline at end of file diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index af4cef1..0000000 --- a/templates/login.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -
- {% if reset_mode %} -
-

Passwort zurĂŒcksetzen

-
- -
- - -
-
- - -
- -
-
- {% elif register_mode %} -
-

Neues Konto erstellen

-
-
- - -
-
- - -
-
- - -
- -
-
- - {% elif registration_success %} -
-

Registrierung erfolgreich

-

Deine ID fĂŒr die Anmeldung lautet:

-

{{ new_identifier }}

-

Bitte speichere oder merke sie dir!

-
- Jetzt anmelden -
-
- {% else %} -
-

Anmeldung

-
-
- - -
-
- - -
- -
-
- - {% endif %} -
-{% endblock %} \ No newline at end of file