Delete all
This commit is contained in:
1 parent
339e0c72ad
commit
6959030639
14 files changed
-2209
No files matched your search
@@ -1,9 +0,0 @@
|
||||
# SCOicide SquadⒷ
|
||||
|
||||
<img src="https://img.shields.io/badge/Made by AI-333333" height="24rem"/>
|
||||
|
||||
> A python Flask based role play game app 🔪
|
||||
|
||||
→ [Live Page](https://example.com/)
|
||||
|
||||
→ [Game rules](rules.md)
|
||||
-653
@@ -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 "<h2>Regelwerk nicht gefunden</h2>"
|
||||
|
||||
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)
|
||||
@@ -1,2 +0,0 @@
|
||||
worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker"
|
||||
workers = 1
|
||||
@@ -1,11 +0,0 @@
|
||||
Flask
|
||||
Flask-SocketIO
|
||||
APScheduler
|
||||
passlib
|
||||
argon2-cffi
|
||||
pyotp
|
||||
qrcode
|
||||
Markdown
|
||||
gunicorn
|
||||
gevent
|
||||
gevent-websocket
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden" viewBox="465.8766784667969 137.75999450683594 353.4313049316406 384.6400146484375"><path d="M811.26 275.62 819.31 380.82 714.1 388.88 712.67 370.11 685.88 388.45 696.13 522.4 612.86 522.4 603.1 394.79 564.72 376.36 561.05 382.31C552.25 394.18 538.55 402.31 522.66 403.53 493.61 405.75 468.26 384 466.03 354.95 463.81 325.9 485.56 300.55 514.61 298.32 536.4 296.65 556.11 308.47 565.54 326.74L567.55 331.82 708.91 321 706.05 283.67ZM659.98 137.76 690.31 188.35 668.03 242.97 658.84 243.67 663.52 304.85 612.47 308.76 607.79 247.58 598.6 248.28 568.27 197.69 590.54 143.07Z" fill="oklch(85% 0.15 110)"/></svg>
|
||||
|
Before Width: | Height: | Size: 747 B |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<section class="administrator-section">
|
||||
{% if setup_required %}
|
||||
<div class="content-card">
|
||||
<h2 class="section-title"><i data-lucide="settings"></i> Administrator Einrichtung</h2>
|
||||
<form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form">
|
||||
<div class="form-group">
|
||||
<label for="password_string" class="input-label">Neues Administrator-Passwort</label>
|
||||
<input type="password" id="password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label>
|
||||
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="check"></i> Einrichtung abschließen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif login_required %}
|
||||
<div class="content-card">
|
||||
<h2 class="section-title"><i data-lucide="shield"></i> Administrator Anmeldung</h2>
|
||||
<form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form">
|
||||
<div class="form-group">
|
||||
<label for="password_string" class="input-label">Passwort</label>
|
||||
<input type="password" id="password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
{% if two_factor_enabled %}
|
||||
<div class="form-group">
|
||||
<label for="two_factor_code" class="input-label">Zwei-Faktor Code</label>
|
||||
<input type="text" id="two_factor_code" name="two_factor_code" class="text-input" required>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="log-in"></i> Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif dashboard_active %}
|
||||
<div class="tab-container">
|
||||
<button class="tab-button active-tab" data-target="control-panel"><i data-lucide="sliders"></i> Steuerung</button>
|
||||
<button class="tab-button" data-target="player-panel"><i data-lucide="users"></i> Spieler</button>
|
||||
<button class="tab-button" data-target="kill-panel"><i data-lucide="skull"></i> Kills</button>
|
||||
</div>
|
||||
|
||||
<div id="control-panel" class="tab-content active-content">
|
||||
<div class="content-card status-card">
|
||||
<h3 class="card-title" style="display: flex; align-items: center; gap: var(--spacing-small);">
|
||||
<i data-lucide="activity"></i> Spielstatus:
|
||||
{% if game_status == 'pending' %}
|
||||
<span class="status-pill warning-pill"><i data-lucide="clock"></i> Ausstehend</span>
|
||||
{% elif game_status == 'running' %}
|
||||
<span class="status-pill success-pill"><i data-lucide="play"></i> Läuft</span>
|
||||
{% elif game_status == 'paused' %}
|
||||
<span class="status-pill warning-pill"><i data-lucide="pause"></i> Pausiert</span>
|
||||
{% elif game_status == 'finished' %}
|
||||
<span class="status-pill danger-pill"><i data-lucide="square"></i> Beendet</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
{% if game_status == 'pending' %}
|
||||
{% if can_start %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="start_game">
|
||||
<button type="submit" class="action-button success-button large-button"><i data-lucide="play"></i> Spiel starten</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="content-card notice-card warning-card">
|
||||
<p class="notice-text">Es werden mindestens zwei freigegebene Spieler benötigt, um das Spiel zu starten.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif game_status == 'running' %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form" style="margin-bottom: var(--spacing-medium);">
|
||||
<input type="hidden" name="action_type" value="pause_game">
|
||||
<button type="submit" class="action-button warning-button large-button"><i data-lucide="pause"></i> Spiel pausieren</button>
|
||||
</form>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="stop_game">
|
||||
<button type="submit" class="action-button danger-button large-button"><i data-lucide="square"></i> Spiel beenden</button>
|
||||
</form>
|
||||
{% elif game_status == 'paused' %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form" style="margin-bottom: var(--spacing-medium);">
|
||||
<input type="hidden" name="action_type" value="resume_game">
|
||||
<button type="submit" class="action-button success-button large-button"><i data-lucide="play"></i> Spiel fortsetzen</button>
|
||||
</form>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="stop_game">
|
||||
<button type="submit" class="action-button danger-button large-button"><i data-lucide="square"></i> Spiel beenden</button>
|
||||
</form>
|
||||
{% elif game_status == 'finished' %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="reset_game">
|
||||
<button type="submit" class="action-button danger-button large-button"><i data-lucide="rotate-ccw"></i> Spiel zurücksetzen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content-card settings-card">
|
||||
<h3 class="card-title"><i data-lucide="shield-check"></i> Sicherheit</h3>
|
||||
|
||||
{% if two_factor_enabled %}
|
||||
<div class="content-card notice-card success-card">
|
||||
<p class="notice-text">Zwei-Faktor-Authentifizierung ist aktiv.</p>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="inline-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="disable_two_factor">
|
||||
<button type="submit" class="action-button danger-button"><i data-lucide="shield-off"></i> Deaktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="content-card notice-card warning-card">
|
||||
<p class="notice-text">Zwei-Faktor-Authentifizierung ist inaktiv. Bitte scanne den QR-Code und bestätige zur Aktivierung.</p>
|
||||
<p>Manuelle Eingabe: <strong>{{ two_factor_secret }}</strong></p>
|
||||
<div class="qr-code-container">
|
||||
{{ qr_code_svg | safe }}
|
||||
</div>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="enable_two_factor">
|
||||
<div class="form-group">
|
||||
<label for="enable_two_factor_code" class="input-label">Bestätigungscode</label>
|
||||
<input type="text" id="enable_two_factor_code" name="two_factor_code" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button success-button"><i data-lucide="shield-alert"></i> Aktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form" style="margin-top: var(--spacing-large);">
|
||||
<input type="hidden" name="action_type" value="update_credentials">
|
||||
<div class="form-group">
|
||||
<label for="new_password_string" class="input-label">Passwort ändern</label>
|
||||
<input type="password" id="new_password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="update_password_confirmation" class="input-label">Passwort wiederholen</label>
|
||||
<input type="password" id="update_password_confirmation" name="password_string_confirmation" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Aktualisieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="player-panel" class="tab-content">
|
||||
<div class="content-card">
|
||||
<h3 class="card-title"><i data-lucide="user-plus"></i> Spieler manuell erstellen</h3>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="create_player">
|
||||
<div class="form-group">
|
||||
<label for="display_name" class="input-label">Anzeigename</label>
|
||||
<input type="text" id="display_name" name="display_name" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="plus"></i> Spieler erstellen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="content-card" style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kennung</th>
|
||||
<th>Name & Änderung</th>
|
||||
<th>Status</th>
|
||||
<th>Ziel</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for current_player in all_players %}
|
||||
<tr class="{% if not current_player.is_approved %}unregistered-row{% endif %}">
|
||||
<td class="monospace-identifier">
|
||||
{{ current_player.player_identifier }}
|
||||
{% if not current_player.is_approved %}
|
||||
<br><span class="status-pill warning-pill" style="margin-left: 0; margin-top: var(--spacing-small);">Wartet</span>
|
||||
{% elif current_player.late_joiner %}
|
||||
<br><span class="status-pill warning-pill" style="margin-left: 0; margin-top: var(--spacing-small);">Späteinstieg</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="edit_name">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<input type="text" name="new_display_name" value="{{ current_player.display_name }}" class="text-input small-input" required>
|
||||
<button type="submit" class="action-button primary-button small-button"><i data-lucide="save"></i></button>
|
||||
</form>
|
||||
{% if current_player.pending_display_name %}
|
||||
<div class="content-card notice-card warning-card" style="margin-top: var(--spacing-small); padding: var(--spacing-small); margin-bottom: 0;">
|
||||
<p style="margin: 0 0 var(--spacing-small) 0; font-size: calc(0.875rem * var(--dimension-multiplier));">
|
||||
Wünscht: <strong>{{ current_player.pending_display_name }}</strong>
|
||||
</p>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-bottom: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="approve_name_change">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button success-button small-button"><i data-lucide="check"></i></button>
|
||||
</form>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="reject_name_change">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button danger-button small-button"><i data-lucide="x"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not current_player.is_approved %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="approve_player">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button success-button small-button"><i data-lucide="user-check"></i> Freigeben</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% if current_player.is_alive %}
|
||||
<span class="status-pill success-pill" style="margin-left: 0;"><i data-lucide="heart"></i> Lebend</span>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="eliminate_player">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button danger-button small-button"><i data-lucide="skull"></i> Töten</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="status-pill danger-pill" style="margin-left: 0;"><i data-lucide="skull"></i> Tot</span>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="revive_player">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button success-button small-button"><i data-lucide="heart-pulse"></i> Beleben</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if current_player.target_identifier %}
|
||||
{{ current_player.target_identifier }}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="change_target">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<select name="new_target_identifier" class="text-input small-input">
|
||||
{% for possible_target in all_players %}
|
||||
{% if possible_target.is_alive and possible_target.player_identifier != current_player.player_identifier %}
|
||||
<option value="{{ possible_target.player_identifier }}" {% if current_player.target_identifier == possible_target.player_identifier %}selected{% endif %}>{{ possible_target.display_name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="action-button secondary-button small-button"><i data-lucide="refresh-cw"></i></button>
|
||||
</form>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="generate_reset">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button secondary-button small-button"><i data-lucide="link"></i> Reset Link</button>
|
||||
</form>
|
||||
{% if current_player.reset_token %}
|
||||
<input type="text" readonly value="{{ url_for('handle_password_reset', token=current_player.reset_token, _external=True) }}" class="text-input small-input" style="margin: var(--spacing-small) 0;">
|
||||
{% endif %}
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="delete_player">
|
||||
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
|
||||
<button type="submit" class="action-button danger-button small-button"><i data-lucide="trash-2"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kill-panel" class="tab-content">
|
||||
<div class="content-card" style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Killer</th>
|
||||
<th>Opfer</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for kill in kill_logs %}
|
||||
<tr>
|
||||
<td>
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="edit_kill">
|
||||
<input type="hidden" name="kill_id" value="{{ kill.kill_id }}">
|
||||
<input type="datetime-local" name="kill_timestamp" value="{{ kill.kill_timestamp[:16] if kill.kill_timestamp else '' }}" class="text-input small-input" required>
|
||||
</td>
|
||||
<td>
|
||||
<select name="new_killer_identifier" class="text-input small-input">
|
||||
<option value="">Keiner (Admin)</option>
|
||||
{% for p in all_players %}
|
||||
<option value="{{ p.player_identifier }}" {% if p.player_identifier == kill.killer_identifier %}selected{% endif %}>{{ p.display_name }} ({{ p.player_identifier }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{{ kill.victim_name or 'Unbekannt' }} ({{ kill.victim_identifier }})
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" class="action-button primary-button small-button" title="Speichern"><i data-lucide="save"></i></button>
|
||||
</form>
|
||||
|
||||
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);">
|
||||
<input type="hidden" name="action_type" value="revoke_kill">
|
||||
<input type="hidden" name="kill_id" value="{{ kill.kill_id }}">
|
||||
<button type="submit" class="action-button danger-button small-button"><i data-lucide="rotate-ccw"></i> Rückgängig</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; opacity: 0.6;">Bisher keine Kills im System.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-confirmation-modal" class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="calendar-clock"></i> Aktion planen</h2>
|
||||
<p class="modal-description">Führe die Aktion sofort aus oder plane sie für die Zukunft.</p>
|
||||
<div class="form-group" style="margin-bottom: var(--spacing-large); text-align: left;">
|
||||
<label for="modal_execution_time" class="input-label">Zeitpunkt (Optional)</label>
|
||||
<input type="datetime-local" id="modal_execution_time" class="text-input time-input" style="width: 100%; box-sizing: border-box;">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="confirm-admin-action" class="action-button success-button"><i data-lucide="check"></i> Bestätigen</button>
|
||||
<button type="button" id="cancel-admin-action" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,187 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<section class="game-section">
|
||||
<div class="content-card">
|
||||
<h2 class="section-title" style="margin: 0;"><i data-lucide="user"></i> Willkommen, {{ player.display_name }} ({{ player.player_identifier }})!</h2>
|
||||
</div>
|
||||
|
||||
{% if not player.is_approved %}
|
||||
<div class="content-card notice-card warning-card">
|
||||
<h2 class="modal-title"><i data-lucide="hourglass"></i> Konto wird geprüft…</h2>
|
||||
</div>
|
||||
{% elif game_status == 'pending' %}
|
||||
<div class="content-card notice-card">
|
||||
<h2 class="modal-title"><i data-lucide="clock"></i> Startet bald…</h2>
|
||||
</div>
|
||||
{% elif game_status == 'finished' %}
|
||||
<div class="content-card leaderboard-card">
|
||||
<h2 class="section-title"><i data-lucide="trophy"></i> Spiel beendet – Rangliste</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Platz</th>
|
||||
<th>Name</th>
|
||||
<th>Punkte</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for current_player in all_players %}
|
||||
<tr class="{% if current_player.player_identifier == session['player_identifier'] %}highlighted-row{% endif %}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ current_player.display_name }}</td>
|
||||
<td>{{ current_player.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% elif game_status in ['running', 'paused'] %}
|
||||
<div class="tab-container">
|
||||
<button class="tab-button active-tab" data-target="mission-panel"><i data-lucide="crosshair"></i> Ziel</button>
|
||||
<button class="tab-button" data-target="leaderboard-panel"><i data-lucide="bar-chart-2"></i> Punkte</button>
|
||||
<button class="tab-button" data-target="rules-panel"><i data-lucide="book-open"></i> Regeln</button>
|
||||
</div>
|
||||
|
||||
<div id="mission-panel" class="tab-content active-content">
|
||||
{% if game_status == 'paused' %}
|
||||
<div class="content-card notice-card warning-card">
|
||||
<h2 class="modal-title"><i data-lucide="pause-circle"></i> Spiel pausiert</h2>
|
||||
<p class="notice-text">Das Spiel wurde pausiert. Eliminierungen sind aktuell nicht möglich.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if player.is_alive %}
|
||||
<div class="content-card target-card" style="text-align: center;">
|
||||
<h3 class="card-subtitle"><i data-lucide="target"></i> Dein Ziel</h3>
|
||||
<h2 class="target-name">{{ target_player.display_name if target_player else 'Niemand' }}</h2>
|
||||
{% if game_status == 'running' %}
|
||||
<button type="button" id="trigger-elimination" class="action-button danger-button large-button"><i data-lucide="crosshair"></i> Kill melden</button>
|
||||
{% else %}
|
||||
<button type="button" class="action-button secondary-button large-button" disabled><i data-lucide="lock"></i> Pausiert</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif player.late_joiner %}
|
||||
<div class="content-card notice-card warning-card">
|
||||
<h2 class="modal-title"><i data-lucide="users"></i> Warte auf Einstieg…</h2>
|
||||
<p class="notice-text">Du bist dem Spiel während einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="content-card notice-card danger-card">
|
||||
<h2 class="modal-title"><i data-lucide="skull"></i> Eliminiert</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content-card statistics-card">
|
||||
<h3 class="section-title"><i data-lucide="pie-chart"></i> Spielübersicht</h3>
|
||||
<p>Überlebende: <strong>{{ alive_count }}</strong></p>
|
||||
<p>Deine Kills: <strong>{{ player.score }}</strong></p>
|
||||
<p>Gesamte Kills: <strong>{{ total_eliminations }}</strong></p>
|
||||
<p>Laufzeit: <strong id="game-stopwatch" data-start-time="{{ start_time }}">Wird berechnet...</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="content-card settings-card">
|
||||
<h3 class="section-title"><i data-lucide="user-cog"></i> Profil</h3>
|
||||
{% if player.pending_display_name %}
|
||||
<div class="notice-card warning-card" style="padding: var(--spacing-medium);">
|
||||
<p class="notice-text" style="margin: 0;">Der Name <strong>{{ player.pending_display_name }}</strong> wird geprüft…</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<button type="button" id="trigger-name-change-modal" class="action-button secondary-button"><i data-lucide="edit-3"></i> Name ändern</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="leaderboard-panel" class="tab-content">
|
||||
<div class="content-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Platz</th>
|
||||
<th>Name</th>
|
||||
<th>Punkte</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for current_player in all_players %}
|
||||
<tr class="{% if current_player.player_identifier == session['player_identifier'] %}highlighted-row{% endif %}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ current_player.display_name }}</td>
|
||||
<td>{{ current_player.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rules-panel" class="tab-content">
|
||||
<div class="content-card markdown-container">
|
||||
{{ rules_markup | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="elimination-confirm-modal" class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="alert-triangle"></i> Kill</h2>
|
||||
<p class="modal-description">Möchtest du melden, dass du dein Ziel gekillt hast?</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="proceed-elimination" class="action-button danger-button"><i data-lucide="crosshair"></i> Ja, melden</button>
|
||||
<button type="button" id="cancel-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="waiting-modal" class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="loader"></i> Bitte warten</h2>
|
||||
<p class="modal-description">Warte auf Bestätigung vom Ziel...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rejection-modal" class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="x-octagon"></i> Abgelehnt</h2>
|
||||
<p class="modal-description">Das Ziel hat deinen Kill abgelehnt.</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="acknowledge-rejection" class="action-button primary-button"><i data-lucide="check"></i> Verstanden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="verification-modal" class="modal-overlay {% if player.pending_attacker_identifier %}active-modal{% endif %}">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="shield-alert"></i> Wurdest du eliminiert?</h2>
|
||||
<p class="modal-description">
|
||||
{% if pending_attacker_name %}
|
||||
<strong>{{ pending_attacker_name }}</strong> hat gemeldet, dass er/sie dich gekillt hat.
|
||||
{% else %}
|
||||
Dein Angreifer hat gemeldet, dass er dich gekillt hat.
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="confirm-elimination" class="action-button danger-button"><i data-lucide="check"></i> Bestätigen</button>
|
||||
<button type="button" id="reject-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="name-change-modal" class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<h2 class="modal-title"><i data-lucide="edit-3"></i> Name ändern</h2>
|
||||
<p class="modal-description">Dein neuer Name muss erst geprüft werden.</p>
|
||||
<form action="{{ url_for('handle_player_action') }}" method="POST" class="input-form asynchronous-form">
|
||||
<input type="hidden" name="action_type" value="request_name_change">
|
||||
<div class="form-group" style="text-align: left;">
|
||||
<label for="pending_display_name" class="input-label">Neuer Name</label>
|
||||
<input type="text" id="pending_display_name" name="pending_display_name" class="text-input" required>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="send"></i> Abschicken</button>
|
||||
<button type="button" id="cancel-name-change" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>SCOicide Squad</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='logo.svg') }}" type="image/svg">
|
||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||
<meta name="theme-color" content="#1a1e29">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='stylesheet.css') }}">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js" defer></script>
|
||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
<script src="{{ url_for('static', filename='behavior.js') }}" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="application-header">
|
||||
<div class="brand-container">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Logo" class="brand-logo">
|
||||
<h1 class="application-title"><i>SCO</i>icide Squad</h1>
|
||||
</div>
|
||||
<nav class="application-navigation">
|
||||
{% if session.get('player_identifier') or session.get('administrator_authenticated') %}
|
||||
<a href="{{ url_for('handle_logout') }}" class="action-button secondary-button icon-button">
|
||||
<i data-lucide="log-out"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
<main class="application-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<section class="authentication-section">
|
||||
{% if reset_mode %}
|
||||
<div class="content-card">
|
||||
<h2 class="section-title"><i data-lucide="key-round"></i> Passwort zurücksetzen</h2>
|
||||
<form action="{{ url_for('handle_password_reset') }}" method="POST" class="input-form">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<div class="form-group">
|
||||
<label for="password_string" class="input-label">Neues Passwort</label>
|
||||
<input type="password" id="password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label>
|
||||
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Passwort speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif register_mode %}
|
||||
<div class="content-card">
|
||||
<h2 class="section-title"><i data-lucide="user-plus"></i> Neues Konto erstellen</h2>
|
||||
<form action="{{ url_for('handle_registration') }}" method="POST" class="input-form">
|
||||
<div class="form-group">
|
||||
<label for="display_name" class="input-label">Dein Name</label>
|
||||
<input type="text" id="display_name" name="display_name" class="text-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_string" class="input-label">Passwort</label>
|
||||
<input type="password" id="password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label>
|
||||
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="check-circle"></i> Registrieren</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="content-card" style="text-align: center;">
|
||||
<a href="{{ url_for('handle_login') }}" class="action-button secondary-button"><i data-lucide="arrow-left"></i> Zurück zur Anmeldung</a>
|
||||
</div>
|
||||
{% elif registration_success %}
|
||||
<div class="content-card notice-card success-card" style="text-align: center;">
|
||||
<h2 class="section-title" style="justify-content: center;"><i data-lucide="party-popper"></i> Registrierung erfolgreich</h2>
|
||||
<p>Deine ID für die Anmeldung lautet:</p>
|
||||
<h1 class="monospace-identifier" style="font-size: calc(3rem * var(--dimension-multiplier)); letter-spacing: calc(0.25rem * var(--dimension-multiplier)); color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); margin: var(--spacing-medium) 0;">{{ new_identifier }}</h1>
|
||||
<p>Bitte speichere oder merke sie dir!</p>
|
||||
<div style="margin-top: var(--spacing-large);">
|
||||
<a href="{{ url_for('handle_login') }}" class="action-button primary-button"><i data-lucide="log-in"></i> Jetzt anmelden</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="content-card">
|
||||
<h2 class="section-title"><i data-lucide="lock"></i> Anmeldung</h2>
|
||||
<form action="{{ url_for('handle_login') }}" method="POST" class="input-form">
|
||||
<div class="form-group">
|
||||
<label for="player_identifier" class="input-label">ID</label>
|
||||
<input type="text" id="player_identifier" name="player_identifier" class="text-input" pattern="[A-Za-z]{{ '{' }}{{ identifier_length }}{{ '}' }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_string" class="input-label">Passwort</label>
|
||||
<input type="password" id="password_string" name="password_string" class="text-input" required>
|
||||
</div>
|
||||
<button type="submit" class="action-button primary-button"><i data-lucide="log-in"></i> Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="content-card" style="text-align: center;">
|
||||
<a href="{{ url_for('handle_registration') }}" class="action-button secondary-button"><i data-lucide="user-plus"></i> Noch kein Konto? Hier registrieren</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user