More smoothness and persistence

This commit is contained in:
MeBeBruno committed 2026-10-04 22:47:53 +02:00
1 parent f924016478
commit aeb9173825
10 files changed
+1021 -684

No files matched your search

+335 -308
View File
@@ -19,25 +19,25 @@ from flask_socketio import SocketIO, emit, join_room
application = Flask(__name__) application = Flask(__name__)
application.secret_key = secrets.token_hex(64) application.secret_key = secrets.token_hex(64)
websocket_manager = SocketIO(application, manage_session=False) websocket_manager = SocketIO(application, manage_session=False)
scheduler_instance = BackgroundScheduler(timezone="Europe/Berlin") scheduler_instance = BackgroundScheduler(timezone='Europe/Berlin')
european_timezone = zoneinfo.ZoneInfo("Europe/Berlin") european_timezone = zoneinfo.ZoneInfo('Europe/Berlin')
IDENTIFIER_LENGTH = 5 IDENTIFIER_LENGTH = 5
def get_database_connection(): def get_database_connection():
connection = sqlite3.connect("game_database.sqlite") connection = sqlite3.connect('game_database.sqlite')
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
return connection return connection
def initialize_database(): def initialize_database():
connection = get_database_connection() connection = get_database_connection()
connection.execute(""" connection.execute('''
CREATE TABLE IF NOT EXISTS game_configuration ( CREATE TABLE IF NOT EXISTS game_configuration (
configuration_key TEXT PRIMARY KEY, configuration_key TEXT PRIMARY KEY,
configuration_value TEXT configuration_value TEXT
) )
""") ''')
connection.execute(""" connection.execute('''
CREATE TABLE IF NOT EXISTS player ( CREATE TABLE IF NOT EXISTS player (
player_identifier TEXT PRIMARY KEY, player_identifier TEXT PRIMARY KEY,
password_hash TEXT, password_hash TEXT,
@@ -51,16 +51,16 @@ def initialize_database():
pending_display_name TEXT, pending_display_name TEXT,
late_joiner INTEGER DEFAULT 0 late_joiner INTEGER DEFAULT 0
) )
""") ''')
connection.execute(""" connection.execute('''
CREATE TABLE IF NOT EXISTS administrator ( CREATE TABLE IF NOT EXISTS administrator (
administrator_identifier INTEGER PRIMARY KEY, administrator_identifier INTEGER PRIMARY KEY,
password_hash TEXT, password_hash TEXT,
two_factor_secret TEXT, two_factor_secret TEXT,
two_factor_enabled INTEGER DEFAULT 0 two_factor_enabled INTEGER DEFAULT 0
) )
""") ''')
connection.execute(""" connection.execute('''
CREATE TABLE IF NOT EXISTS kill_log ( CREATE TABLE IF NOT EXISTS kill_log (
kill_identifier INTEGER PRIMARY KEY AUTOINCREMENT, kill_identifier INTEGER PRIMARY KEY AUTOINCREMENT,
killer_identifier TEXT, killer_identifier TEXT,
@@ -69,483 +69,492 @@ def initialize_database():
kill_timestamp TEXT, kill_timestamp TEXT,
report_status TEXT DEFAULT 'confirmed' report_status TEXT DEFAULT 'confirmed'
) )
""") ''')
try: try:
connection.execute("ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0") connection.execute('ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0')
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
try: try:
connection.execute("ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0") connection.execute('ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0')
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
try: try:
connection.execute("ALTER TABLE player ADD COLUMN pending_display_name TEXT") connection.execute('ALTER TABLE player ADD COLUMN pending_display_name TEXT')
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
try: try:
connection.execute("ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0") connection.execute('ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0')
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
try: try:
connection.execute("ALTER TABLE kill_log ADD COLUMN report_status TEXT DEFAULT 'confirmed'") connection.execute('ALTER TABLE kill_log ADD COLUMN report_status TEXT DEFAULT "confirmed"')
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
cursor = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'") cursor = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"')
if not cursor.fetchone(): 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 ("game_status", "pending")')
connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('start_time', '')") connection.execute('INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ("start_time", "")')
connection.commit() connection.commit()
connection.close() connection.close()
def require_player_authentication(function): def require_player_authentication(function):
def wrapper(*arguments, **keyword_arguments): def wrapper(*arguments, **keyword_arguments):
if "player_identifier" not in session: if 'player_identifier' not in session:
return redirect(url_for("handle_login")) return redirect(url_for('handle_login'))
return function(*arguments, **keyword_arguments) return function(*arguments, **keyword_arguments)
wrapper.__name__ = function.__name__ wrapper.__name__ = function.__name__
return wrapper return wrapper
def require_administrator_authentication(function): def require_administrator_authentication(function):
def wrapper(*arguments, **keyword_arguments): def wrapper(*arguments, **keyword_arguments):
if not session.get("administrator_authenticated"): if not session.get('administrator_authenticated'):
return redirect(url_for("handle_administrator")) return redirect(url_for('handle_administrator'))
return function(*arguments, **keyword_arguments) return function(*arguments, **keyword_arguments)
wrapper.__name__ = function.__name__ wrapper.__name__ = function.__name__
return wrapper return wrapper
def get_markdown_rules(): def get_markdown_rules():
if os.path.exists("rules.md"): if os.path.exists('rules.md'):
with open("rules.md", "r", encoding="utf-8") as markdown_file: with open('rules.md', 'r', encoding='utf-8') as markdown_file:
return markdown.markdown(markdown_file.read()) return markdown.markdown(markdown_file.read())
return "<h2>Regelwerk nicht gefunden</h2>" return '<h2>Regelwerk nicht gefunden</h2>'
def shuffle_player_targets(): def shuffle_player_targets():
connection = get_database_connection() connection = get_database_connection()
players = connection.execute("SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall() players = connection.execute('SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1').fetchall()
if len(players) > 1: if len(players) > 1:
identifiers = [player_record["player_identifier"] for player_record in players] identifiers = [player_record['player_identifier'] for player_record in players]
secure_random_generator = secrets.SystemRandom() secure_random_generator = secrets.SystemRandom()
secure_random_generator.shuffle(identifiers) secure_random_generator.shuffle(identifiers)
for index, identifier in enumerate(identifiers): for index, identifier in enumerate(identifiers):
target = identifiers[(index + 1) % len(identifiers)] target = identifiers[(index + 1) % len(identifiers)]
connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target, identifier)) connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (target, identifier))
connection.commit() connection.commit()
connection.close() connection.close()
websocket_manager.emit("game_state_changed") websocket_manager.emit('game_state_changed')
def process_administrator_action(action_type, form_data): def process_administrator_action(action_type, form_data):
connection = get_database_connection() connection = get_database_connection()
has_game_state_changed = False has_game_state_changed = False
if action_type == "start_game": if action_type == 'start_game':
alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone() alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1').fetchone()
if alive_count_record["alive_count"] >= 2: 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 = "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.execute('UPDATE game_configuration SET configuration_value = ? WHERE configuration_key = "start_time"', (datetime.now(european_timezone).isoformat(),))
connection.commit() connection.commit()
connection.close() connection.close()
shuffle_player_targets() shuffle_player_targets()
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('admin_state_changed', to='administrator_room')
return return
elif action_type == "stop_game": elif action_type == 'stop_game':
connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "pause_game": elif action_type == 'pause_game':
connection.execute("UPDATE game_configuration SET configuration_value = 'paused' WHERE configuration_key = 'game_status'") connection.execute('UPDATE game_configuration SET configuration_value = "paused" WHERE configuration_key = "game_status"')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "resume_game": elif action_type == 'resume_game':
connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") connection.execute('UPDATE game_configuration SET configuration_value = "running" WHERE configuration_key = "game_status"')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "reset_game": 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 = "pending" WHERE configuration_key = "game_status"')
connection.execute("UPDATE game_configuration SET configuration_value = '' WHERE configuration_key = 'start_time'") 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('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.execute('DELETE FROM kill_log')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "create_player": elif action_type == 'create_player':
display_name = form_data.get("display_name") display_name = form_data.get('display_name')
new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH))
temporary_password = secrets.token_urlsafe(16) temporary_password = secrets.token_urlsafe(16)
hashed_password = argon2.hash(temporary_password) 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.execute('INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)', (new_identifier, hashed_password, display_name))
connection.commit() connection.commit()
elif action_type == "approve_player": elif action_type == 'approve_player':
target_identifier = form_data.get("player_identifier") target_identifier = form_data.get('player_identifier')
status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() 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_late_joiner = 1 if status_record['configuration_value'] in ['running', 'paused'] else 0
is_alive_status = 0 if is_late_joiner else 1 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.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
websocket_manager.emit('game_state_changed', to=target_identifier)
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() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "edit_name": elif action_type == 'approve_name_change':
target_identifier = form_data.get("player_identifier") target_identifier = form_data.get('player_identifier')
new_name = form_data.get("new_display_name") connection.execute('UPDATE player SET display_name = pending_display_name, pending_display_name = NULL WHERE player_identifier = ?', (target_identifier,))
connection.execute("UPDATE player SET display_name = ? WHERE player_identifier = ?", (new_name, target_identifier))
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "approve_name_change": elif action_type == 'reject_name_change':
target_identifier = form_data.get("player_identifier") 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.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() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "reject_name_change": elif action_type == 'revive_player':
target_identifier = form_data.get("player_identifier") target_identifier = form_data.get('player_identifier')
connection.execute("UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,)) player_to_revive = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (target_identifier,)).fetchone()
connection.commit() if player_to_revive and player_to_revive['is_alive'] == 0:
websocket_manager.emit("game_state_changed", to=target_identifier) alive_players = connection.execute('SELECT * FROM player WHERE is_alive = 1 AND is_approved = 1').fetchall()
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: if len(alive_players) > 0:
random_living_player = secrets.choice(alive_players) random_living_player = secrets.choice(alive_players)
random_living_identifier = random_living_player["player_identifier"] random_living_identifier = random_living_player['player_identifier']
attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (random_living_identifier,)).fetchone() attacker = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (random_living_identifier,)).fetchone()
if attacker: if attacker:
connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target_identifier, attacker["player_identifier"])) 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)) connection.execute('UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = ? WHERE player_identifier = ?', (random_living_identifier, target_identifier))
else: else:
connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?", (target_identifier,)) connection.execute('UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?', (target_identifier,))
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "eliminate_player": elif action_type == 'eliminate_player':
target_identifier = form_data.get("player_identifier") target_identifier = form_data.get('player_identifier')
victim = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone() victim = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (target_identifier,)).fetchone()
if victim and victim["is_alive"] == 1: if victim and victim['is_alive'] == 1:
attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_identifier,)).fetchone() attacker = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (target_identifier,)).fetchone()
new_target = victim["target_identifier"] 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,)) connection.execute('UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?', (target_identifier,))
attacker_identifier = None attacker_identifier = None
if attacker: if attacker:
attacker_identifier = attacker["player_identifier"] attacker_identifier = attacker['player_identifier']
connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target, attacker_identifier)) connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (new_target, attacker_identifier))
timestamp = datetime.now(european_timezone).isoformat() timestamp = datetime.now(european_timezone).isoformat()
connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, 'confirmed')", (attacker_identifier, target_identifier, new_target, timestamp)) connection.execute('INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, "confirmed")', (attacker_identifier, 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() 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: if alive_count_record['alive_count'] <= 1:
status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() 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" 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'") 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': 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.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "delete_report": elif action_type == 'delete_report':
kill_identifier = form_data.get("kill_identifier") kill_identifier = form_data.get('kill_identifier')
report = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() report = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone()
if report and report["report_status"] == 'pending': if report and report['report_status'] == 'pending':
connection.execute("UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?", (report["victim_identifier"],)) connection.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (report['victim_identifier'],))
connection.execute("DELETE FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)) connection.execute('DELETE FROM kill_log WHERE kill_identifier = ?', (kill_identifier,))
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "force_confirm_report": elif action_type == 'force_confirm_report':
kill_identifier = form_data.get("kill_identifier") kill_identifier = form_data.get('kill_identifier')
report = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() report = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone()
if report and report["report_status"] == 'pending': if report and report['report_status'] == 'pending':
attacker_identifier = report["killer_identifier"] attacker_identifier = report['killer_identifier']
victim_identifier = report["victim_identifier"] victim_identifier = report['victim_identifier']
victim = connection.execute("SELECT target_identifier, is_alive FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() victim = connection.execute('SELECT target_identifier, is_alive FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone()
if victim and victim["is_alive"] == 1: if victim and victim['is_alive'] == 1:
new_target = victim["target_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 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)) connection.execute('UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?', (new_target, attacker_identifier))
timestamp = datetime.now(european_timezone).isoformat() timestamp = datetime.now(european_timezone).isoformat()
connection.execute("UPDATE kill_log SET report_status = 'confirmed', kill_timestamp = ? WHERE kill_identifier = ?", (timestamp, kill_identifier)) connection.execute('UPDATE kill_log SET report_status = "confirmed", kill_timestamp = ? WHERE kill_identifier = ?', (timestamp, kill_identifier))
alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() 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: if alive_count_record['alive_count'] <= 1:
status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() 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" 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'") 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': 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.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1')
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "revoke_kill": elif action_type == 'revoke_kill':
kill_identifier = form_data.get("kill_identifier") kill_identifier = form_data.get('kill_identifier')
kill_record = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() kill_record = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone()
if kill_record and kill_record["report_status"] == 'confirmed': if kill_record and kill_record['report_status'] == 'confirmed':
if kill_record["killer_identifier"]: if kill_record['killer_identifier']:
connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (kill_record["killer_identifier"],)) connection.execute('UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?', (kill_record['killer_identifier'],))
victim_identifier = kill_record["victim_identifier"] victim_identifier = kill_record['victim_identifier']
target_to_restore = kill_record["victim_previous_target"] 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() 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_identifier)) connection.execute('UPDATE player SET is_alive = 1, target_identifier = ? WHERE player_identifier = ?', (target_to_restore, victim_identifier))
if player_pointing_to_target: if player_pointing_to_target:
connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (victim_identifier, player_pointing_to_target["player_identifier"])) connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (victim_identifier, player_pointing_to_target['player_identifier']))
connection.execute("UPDATE kill_log SET report_status = 'revoked' WHERE kill_identifier = ?", (kill_identifier,)) connection.execute('UPDATE kill_log SET report_status = "revoked" WHERE kill_identifier = ?', (kill_identifier,))
alive_count = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()["alive_count"] 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: if alive_count > 1:
status = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()["configuration_value"] status = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone()['configuration_value']
if status == 'finished': if status == 'finished':
connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") 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_identifier,)) connection.execute('UPDATE player SET score = MAX(0, score - 3) WHERE is_alive = 1 AND is_approved = 1 AND player_identifier != ?', (victim_identifier,))
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "change_target": elif action_type == 'change_target':
player_identifier = form_data.get("player_identifier") player_identifier = form_data.get('player_identifier')
new_target_identifier = form_data.get("new_target_identifier") new_target_identifier = form_data.get('new_target_identifier')
if player_identifier and new_target_identifier and player_identifier != 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() 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() other_player_record = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (new_target_identifier,)).fetchone()
if player_record and other_player_record: 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 = ?', (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.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (player_record['target_identifier'], other_player_record['player_identifier']))
connection.commit() connection.commit()
has_game_state_changed = True has_game_state_changed = True
elif action_type == "generate_reset": elif action_type == 'generate_reset':
target_identifier = form_data.get("player_identifier") target_identifier = form_data.get('player_identifier')
reset_token = secrets.token_urlsafe(32) reset_token = secrets.token_urlsafe(32)
connection.execute("UPDATE player SET reset_token = ? WHERE player_identifier = ?", (reset_token, target_identifier)) connection.execute('UPDATE player SET reset_token = ? WHERE player_identifier = ?', (reset_token, target_identifier))
connection.commit() connection.commit()
elif action_type == "update_credentials": elif action_type == 'update_credentials':
new_password = form_data.get("password_string") new_password = form_data.get('password_string')
password_confirmation = form_data.get("password_string_confirmation") password_confirmation = form_data.get('password_string_confirmation')
if new_password and new_password == password_confirmation: if new_password and new_password == password_confirmation:
hashed_password = argon2.hash(new_password) hashed_password = argon2.hash(new_password)
connection.execute("UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1", (hashed_password,)) connection.execute('UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1', (hashed_password,))
connection.commit() connection.commit()
elif action_type == "enable_two_factor": elif action_type == 'enable_two_factor':
two_factor_input = form_data.get("two_factor_code") two_factor_input = form_data.get('two_factor_code')
administrator_record = connection.execute("SELECT two_factor_secret FROM administrator WHERE administrator_identifier = 1").fetchone() 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"]) time_based_one_time_password_validator = pyotp.TOTP(administrator_record['two_factor_secret'])
if time_based_one_time_password_validator.verify(two_factor_input): 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.execute('UPDATE administrator SET two_factor_enabled = 1 WHERE administrator_identifier = 1')
connection.commit() connection.commit()
elif action_type == "disable_two_factor": elif action_type == 'disable_two_factor':
connection.execute("UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1") connection.execute('UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1')
connection.commit() connection.commit()
connection.close() connection.close()
if has_game_state_changed: if has_game_state_changed:
websocket_manager.emit("game_state_changed") websocket_manager.emit('game_state_changed')
else: else:
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('admin_state_changed', to='administrator_room')
def scheduled_action_wrapper(action_type, form_data): def scheduled_action_wrapper(action_type, form_data):
with application.app_context(): with application.app_context():
process_administrator_action(action_type, form_data) process_administrator_action(action_type, form_data)
@application.route("/service_worker.js") @application.route('/service_worker.js')
def serve_service_worker(): def serve_service_worker():
return send_from_directory(application.static_folder, "service_worker.js") return send_from_directory(application.static_folder, 'service_worker.js')
@application.route("/") @application.route('/')
@require_player_authentication @require_player_authentication
def handle_index(): def handle_index():
connection = get_database_connection() connection = get_database_connection()
player = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (session["player_identifier"],)).fetchone() player = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (session['player_identifier'],)).fetchone()
if not player: if not player:
session.clear() session.clear()
return redirect(url_for("handle_login")) return redirect(url_for('handle_login'))
status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() status_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone()
game_status = status_row["configuration_value"] game_status = status_row['configuration_value']
time_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'start_time'").fetchone() time_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "start_time"').fetchone()
start_time = time_row["configuration_value"] start_time = time_row['configuration_value']
target_player = None target_player = None
if player["target_identifier"]: if player['target_identifier']:
target_player = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["target_identifier"],)).fetchone() target_player = connection.execute('SELECT display_name FROM player WHERE player_identifier = ?', (player['target_identifier'],)).fetchone()
pending_attacker_name = None pending_attacker_name = None
if player["pending_attacker_identifier"]: if player['pending_attacker_identifier']:
attacker = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["pending_attacker_identifier"],)).fetchone() attacker = connection.execute('SELECT display_name FROM player WHERE player_identifier = ?', (player['pending_attacker_identifier'],)).fetchone()
if attacker: if attacker:
pending_attacker_name = attacker["display_name"] pending_attacker_name = attacker['display_name']
all_players = connection.execute("SELECT * FROM player WHERE is_approved = 1 ORDER BY score DESC").fetchall() pending_kill_record = connection.execute('SELECT kill_identifier FROM kill_log WHERE killer_identifier = ? AND report_status = "pending"', (player['player_identifier'],)).fetchone()
total_eliminations = sum(player_record["score"] for player_record in all_players) has_pending_kill_report = pending_kill_record is not None
alive_count = sum(1 for player_record in all_players if player_record["is_alive"] == 1)
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() connection.close()
document_markup = get_markdown_rules() document_markup = get_markdown_rules()
return render_template( return render_template(
"game.html", 'game.html',
player=player, player=player,
game_status=game_status, game_status=game_status,
start_time=start_time, start_time=start_time,
target_player=target_player, target_player=target_player,
pending_attacker_name=pending_attacker_name, pending_attacker_name=pending_attacker_name,
has_pending_kill_report=has_pending_kill_report,
all_players=all_players, all_players=all_players,
total_eliminations=total_eliminations, total_eliminations=total_eliminations,
alive_count=alive_count, alive_count=alive_count,
rules_markup=document_markup rules_markup=document_markup
) )
@application.route("/register", methods=["GET", "POST"]) @application.route('/register', methods=['GET', 'POST'])
def handle_registration(): def handle_registration():
if request.method == "POST": if request.method == 'POST':
display_name = request.form.get("display_name") display_name = request.form.get('display_name')
password_input = request.form.get("password_string") password_input = request.form.get('password_string')
password_confirmation = request.form.get("password_string_confirmation") password_confirmation = request.form.get('password_string_confirmation')
if password_input and password_input == password_confirmation: if password_input and password_input == password_confirmation:
new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH))
hashed_password = argon2.hash(password_input) hashed_password = argon2.hash(password_input)
connection = get_database_connection() 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.execute('INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)', (new_identifier, hashed_password, display_name))
connection.commit() connection.commit()
connection.close() connection.close()
websocket_manager.emit("admin_state_changed", to="administrator_room") 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, registration_success=True, new_identifier=new_identifier)
return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, register_mode=True) return render_template('login.html', identifier_length=IDENTIFIER_LENGTH, register_mode=True)
@application.route("/login", methods=["GET", "POST"]) @application.route('/login', methods=['GET', 'POST'])
def handle_login(): def handle_login():
if request.method == "POST": if request.method == 'POST':
identifier_input = request.form.get("player_identifier", "").strip().upper() identifier_input = request.form.get('player_identifier', '').strip().upper()
password_input = request.form.get("password_string", "") password_input = request.form.get('password_string', '')
connection = get_database_connection() connection = get_database_connection()
player = connection.execute("SELECT password_hash FROM player WHERE player_identifier = ?", (identifier_input,)).fetchone() player = connection.execute('SELECT password_hash FROM player WHERE player_identifier = ?', (identifier_input,)).fetchone()
connection.close() connection.close()
if player and argon2.verify(password_input, player["password_hash"]): if player and argon2.verify(password_input, player['password_hash']):
session["player_identifier"] = identifier_input session['player_identifier'] = identifier_input
return redirect(url_for("handle_index")) return redirect(url_for('handle_index'))
return render_template("login.html", identifier_length=IDENTIFIER_LENGTH) return render_template('login.html', identifier_length=IDENTIFIER_LENGTH)
@application.route("/reset_password", methods=["GET", "POST"]) @application.route('/reset_password', methods=['GET', 'POST'])
def handle_password_reset(): def handle_password_reset():
token = request.args.get("token") or request.form.get("token") token = request.args.get('token') or request.form.get('token')
if not token: if not token:
abort(400) abort(400)
connection = get_database_connection() connection = get_database_connection()
player = connection.execute("SELECT player_identifier FROM player WHERE reset_token = ?", (token,)).fetchone() player = connection.execute('SELECT player_identifier FROM player WHERE reset_token = ?', (token,)).fetchone()
if not player: if not player:
connection.close() connection.close()
abort(403) abort(403)
if request.method == "POST": if request.method == 'POST':
new_password = request.form.get("password_string") new_password = request.form.get('password_string')
password_confirmation = request.form.get("password_string_confirmation") password_confirmation = request.form.get('password_string_confirmation')
if new_password and new_password == password_confirmation: if new_password and new_password == password_confirmation:
hashed_password = argon2.hash(new_password) hashed_password = argon2.hash(new_password)
connection.execute("UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?", (hashed_password, token)) connection.execute('UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?', (hashed_password, token))
connection.commit() connection.commit()
connection.close() connection.close()
return redirect(url_for("handle_login")) return redirect(url_for('handle_login'))
connection.close() connection.close()
return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token) return render_template('login.html', identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token)
@application.route("/player/action", methods=["POST"]) @application.route('/player/action', methods=['POST'])
@require_player_authentication @require_player_authentication
def handle_player_action(): def handle_player_action():
action_type = request.form.get("action_type") action_type = request.form.get('action_type')
connection = get_database_connection() connection = get_database_connection()
if action_type == "request_name_change": if action_type == 'request_name_change':
pending_name = request.form.get("pending_display_name") 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.execute('UPDATE player SET pending_display_name = ? WHERE player_identifier = ?', (pending_name, session['player_identifier']))
connection.commit() connection.commit()
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('admin_state_changed', to='administrator_room')
elif action_type == 'cancel_name_change':
connection.execute('UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?', (session['player_identifier'],))
connection.commit()
websocket_manager.emit('admin_state_changed', to='administrator_room')
connection.close() connection.close()
return redirect(url_for("handle_index")) return redirect(url_for('handle_index'))
@application.route("/logout") @application.route('/logout')
def handle_logout(): def handle_logout():
session.clear() session.clear()
return redirect(url_for("handle_login")) return redirect(url_for('handle_login'))
@application.route("/admin", methods=["GET", "POST"]) @application.route('/admin', methods=['GET', 'POST'])
def handle_administrator(): def handle_administrator():
connection = get_database_connection() connection = get_database_connection()
administrator_record = connection.execute("SELECT * FROM administrator WHERE administrator_identifier = 1").fetchone() administrator_record = connection.execute('SELECT * FROM administrator WHERE administrator_identifier = 1').fetchone()
if not administrator_record: if not administrator_record:
if request.method == "POST": if request.method == 'POST':
password_input = request.form.get("password_string") password_input = request.form.get('password_string')
password_confirmation = request.form.get("password_string_confirmation") password_confirmation = request.form.get('password_string_confirmation')
if password_input and password_input == password_confirmation: if password_input and password_input == password_confirmation:
hashed_password = argon2.hash(password_input) hashed_password = argon2.hash(password_input)
two_factor_secret = pyotp.random_base32() 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.execute('INSERT INTO administrator (administrator_identifier, password_hash, two_factor_secret, two_factor_enabled) VALUES (1, ?, ?, 0)', (hashed_password, two_factor_secret))
connection.commit() connection.commit()
session["administrator_authenticated"] = True session['administrator_authenticated'] = True
connection.close() connection.close()
return redirect(url_for("handle_administrator")) return redirect(url_for('handle_administrator'))
connection.close() connection.close()
return render_template("administrator.html", setup_required=True) return render_template('administrator.html', setup_required=True)
if not session.get("administrator_authenticated"): if not session.get('administrator_authenticated'):
if request.method == "POST": if request.method == 'POST':
password_input = request.form.get("password_string") password_input = request.form.get('password_string')
if argon2.verify(password_input, administrator_record["password_hash"]): if argon2.verify(password_input, administrator_record['password_hash']):
if administrator_record["two_factor_enabled"]: if administrator_record['two_factor_enabled']:
two_factor_input = request.form.get("two_factor_code") two_factor_input = request.form.get('two_factor_code')
time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"]) time_based_one_time_password_validator = pyotp.TOTP(administrator_record['two_factor_secret'])
if time_based_one_time_password_validator.verify(two_factor_input): if time_based_one_time_password_validator.verify(two_factor_input):
session["administrator_authenticated"] = True session['administrator_authenticated'] = True
connection.close() connection.close()
return redirect(url_for("handle_administrator")) return redirect(url_for('handle_administrator'))
else: else:
session["administrator_authenticated"] = True session['administrator_authenticated'] = True
connection.close() connection.close()
return redirect(url_for("handle_administrator")) return redirect(url_for('handle_administrator'))
connection.close() connection.close()
return render_template("administrator.html", login_required=True, two_factor_enabled=bool(administrator_record["two_factor_enabled"])) 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() status_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone()
game_status = status_row["configuration_value"] game_status = status_row['configuration_value']
all_players = connection.execute("SELECT * FROM player ORDER BY is_approved ASC, display_name ASC").fetchall() 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() 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 can_start = alive_count_record['alive_count'] >= 2
kill_logs = connection.execute(""" kill_logs = connection.execute('''
SELECT k.*, SELECT k.*,
killer.display_name as killer_name, killer.display_name as killer_name,
victim.display_name as victim_name victim.display_name as victim_name
@@ -553,120 +562,138 @@ def handle_administrator():
LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier
LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier
ORDER BY k.kill_timestamp DESC ORDER BY k.kill_timestamp DESC
""").fetchall() ''').fetchall()
two_factor_uri = pyotp.totp.TOTP(administrator_record["two_factor_secret"]).provisioning_uri(name="Administrator", issuer_name="AssassinGame") 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_factory = qrcode.image.svg.SvgPathImage
qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory) qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory)
qr_stream = io.BytesIO() qr_stream = io.BytesIO()
qr_image.save(qr_stream) qr_image.save(qr_stream)
qr_code_svg = qr_stream.getvalue().decode("utf-8") qr_code_svg = qr_stream.getvalue().decode('utf-8')
connection.close() connection.close()
return render_template( return render_template(
"administrator.html", 'administrator.html',
dashboard_active=True, dashboard_active=True,
game_status=game_status, game_status=game_status,
can_start=can_start, can_start=can_start,
all_players=all_players, all_players=all_players,
kill_logs=kill_logs, kill_logs=kill_logs,
two_factor_secret=administrator_record["two_factor_secret"], two_factor_secret=administrator_record['two_factor_secret'],
two_factor_enabled=bool(administrator_record["two_factor_enabled"]), two_factor_enabled=bool(administrator_record['two_factor_enabled']),
qr_code_svg=qr_code_svg qr_code_svg=qr_code_svg
) )
@application.route("/admin/action", methods=["POST"]) @application.route('/admin/action', methods=['POST'])
@require_administrator_authentication @require_administrator_authentication
def handle_administrator_action(): def handle_administrator_action():
form_data = request.form.to_dict() form_data = request.form.to_dict()
action_type = form_data.get("action_type") action_type = form_data.get('action_type')
execution_time_string = form_data.get("execution_time") execution_time_string = form_data.get('execution_time')
if execution_time_string: if execution_time_string:
execution_datetime = datetime.strptime(execution_time_string[:16], "%Y-%m-%dT%H:%M").replace(tzinfo=european_timezone) 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]) scheduler_instance.add_job(scheduled_action_wrapper, DateTrigger(run_date=execution_datetime), args=[action_type, form_data])
else: else:
process_administrator_action(action_type, form_data) process_administrator_action(action_type, form_data)
return redirect(url_for("handle_administrator")) return redirect(url_for('handle_administrator'))
@websocket_manager.on("connect") @websocket_manager.on('connect')
def handle_websocket_connection(): def handle_websocket_connection():
if "player_identifier" in session: if 'player_identifier' in session:
join_room(session["player_identifier"]) join_room(session['player_identifier'])
if session.get("administrator_authenticated"): if session.get('administrator_authenticated'):
join_room("administrator_room") join_room('administrator_room')
@websocket_manager.on("request_elimination") @websocket_manager.on('request_elimination')
def handle_elimination_request(): def handle_elimination_request():
attacker_identifier = session.get("player_identifier") attacker_identifier = session.get('player_identifier')
if not attacker_identifier: if not attacker_identifier:
return return
connection = get_database_connection() connection = get_database_connection()
attacker = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (attacker_identifier,)).fetchone() attacker = connection.execute('SELECT target_identifier FROM player WHERE player_identifier = ?', (attacker_identifier,)).fetchone()
if attacker and attacker["target_identifier"]: if attacker and attacker['target_identifier']:
victim_identifier = attacker["target_identifier"] victim_identifier = attacker['target_identifier']
victim = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() victim = connection.execute('SELECT target_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone()
connection.execute("UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?", (attacker_identifier, victim_identifier)) connection.execute('UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?', (attacker_identifier, victim_identifier))
timestamp = datetime.now(european_timezone).isoformat() timestamp = datetime.now(european_timezone).isoformat()
connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, 'pending')", (attacker_identifier, victim_identifier, victim["target_identifier"], timestamp)) connection.execute('INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, "pending")', (attacker_identifier, victim_identifier, victim['target_identifier'], timestamp))
connection.commit() connection.commit()
emit("elimination_verification_requested", to=victim_identifier) emit('elimination_verification_requested', to=victim_identifier)
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('game_state_changed', to=attacker_identifier)
websocket_manager.emit('admin_state_changed', to='administrator_room')
connection.close() connection.close()
@websocket_manager.on("confirm_elimination") @websocket_manager.on('revoke_elimination_request')
def handle_elimination_revocation():
attacker_identifier = session.get('player_identifier')
if not attacker_identifier:
return
connection = get_database_connection()
victim = connection.execute('SELECT player_identifier FROM player WHERE pending_attacker_identifier = ?', (attacker_identifier,)).fetchone()
if victim:
connection.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim['player_identifier'],))
connection.execute('DELETE FROM kill_log WHERE killer_identifier = ? AND report_status = "pending"', (attacker_identifier,))
connection.commit()
emit('elimination_revoked', to=victim['player_identifier'])
websocket_manager.emit('game_state_changed', to=attacker_identifier)
websocket_manager.emit('admin_state_changed', to='administrator_room')
connection.close()
@websocket_manager.on('confirm_elimination')
def handle_elimination_confirmation(): def handle_elimination_confirmation():
victim_identifier = session.get("player_identifier") victim_identifier = session.get('player_identifier')
if not victim_identifier: if not victim_identifier:
return return
connection = get_database_connection() connection = get_database_connection()
victim = connection.execute("SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() victim = connection.execute('SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone()
if victim and victim["pending_attacker_identifier"]: if victim and victim['pending_attacker_identifier']:
attacker_identifier = victim["pending_attacker_identifier"] attacker_identifier = victim['pending_attacker_identifier']
new_target = victim["target_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 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)) connection.execute('UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?', (new_target, attacker_identifier))
timestamp = datetime.now(european_timezone).isoformat() timestamp = datetime.now(european_timezone).isoformat()
connection.execute("UPDATE kill_log SET report_status = 'confirmed', kill_timestamp = ? WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = 'pending'", (timestamp, attacker_identifier, victim_identifier)) connection.execute('UPDATE kill_log SET report_status = "confirmed", kill_timestamp = ? WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = "pending"', (timestamp, attacker_identifier, victim_identifier))
alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() 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: if alive_count_record['alive_count'] <= 1:
status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() 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" 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'") 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': 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.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1')
connection.commit() connection.commit()
websocket_manager.emit("game_state_changed") websocket_manager.emit('game_state_changed')
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('admin_state_changed', to='administrator_room')
connection.close() connection.close()
@websocket_manager.on("reject_elimination") @websocket_manager.on('reject_elimination')
def handle_elimination_rejection(): def handle_elimination_rejection():
victim_identifier = session.get("player_identifier") victim_identifier = session.get('player_identifier')
if not victim_identifier: if not victim_identifier:
return return
connection = get_database_connection() connection = get_database_connection()
victim = connection.execute("SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() victim = connection.execute('SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone()
if victim and victim["pending_attacker_identifier"]: if victim and victim['pending_attacker_identifier']:
attacker_identifier = 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.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim_identifier,))
connection.execute("UPDATE kill_log SET report_status = 'rejected' WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = 'pending'", (attacker_identifier, victim_identifier)) connection.execute('UPDATE kill_log SET report_status = "rejected" WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = "pending"', (attacker_identifier, victim_identifier))
connection.commit() connection.commit()
emit("elimination_rejected", to=attacker_identifier) emit('elimination_rejected', to=attacker_identifier)
websocket_manager.emit("admin_state_changed", to="administrator_room") websocket_manager.emit('game_state_changed', to=victim_identifier)
websocket_manager.emit('admin_state_changed', to='administrator_room')
connection.close() connection.close()
if __name__ == "__main__": if __name__ == '__main__':
initialize_database() initialize_database()
scheduler_instance.start() scheduler_instance.start()
websocket_manager.run(application, host="0.0.0.0", port=5000) websocket_manager.run(application, host='0.0.0.0', port=5000)
+87
View File
@@ -0,0 +1,87 @@
import pathlib
def is_item_ignored(item_name, ignored_directories_list, ignored_extensions_list):
if item_name in ignored_directories_list:
return True
for extension_string in ignored_extensions_list:
if item_name.endswith(extension_string):
return True
return False
def format_tree_line(entry_name, prefix_string, is_last_element):
connector_string = '└── ' if is_last_element else '├── '
return prefix_string + connector_string + entry_name
def format_file_section(relative_path_string, file_content_string):
header_string = '\n--- FILE START: ' + relative_path_string + ' ---\n'
footer_string = '\n--- FILE END: ' + relative_path_string + ' ---\n'
return header_string + file_content_string + footer_string
def build_final_output_string(tree_lines_list, code_contents_list):
tree_string = '\n'.join(tree_lines_list)
code_string = ''.join(code_contents_list)
return 'PROJECT STRUCTURE:\n' + tree_string + '\n\n' + code_string
def collect_file_system_nodes(root_path, ignored_directories_list, ignored_extensions_list):
valid_nodes_list = []
try:
directory_items_list = list(root_path.iterdir())
except PermissionError:
return valid_nodes_list
for item_path in directory_items_list:
if not is_item_ignored(item_path.name, ignored_directories_list, ignored_extensions_list):
valid_nodes_list.append(item_path)
valid_nodes_list.sort(key=lambda path_object: (not path_object.is_dir(), path_object.name.lower()))
return valid_nodes_list
def read_file_content_safely(file_path_object):
try:
return file_path_object.read_text(encoding='utf-8')
except UnicodeDecodeError:
return '[BINARY_OR_UNSUPPORTED_ENCODING]'
except PermissionError:
return '[PERMISSION_DENIED]'
def write_output_file(output_file_path, content_string):
output_file_path.write_text(content_string, encoding='utf-8')
def process_directory_recursively(current_path, base_directory_path, prefix_string, ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list):
nodes_list = collect_file_system_nodes(current_path, ignored_directories_list, ignored_extensions_list)
total_nodes_count = len(nodes_list)
for index_number, node_path in enumerate(nodes_list):
is_last_node = index_number == total_nodes_count - 1
formatted_tree_line = format_tree_line(node_path.name, prefix_string, is_last_node)
tree_lines_list.append(formatted_tree_line)
if node_path.is_dir():
new_prefix_string = prefix_string + (' ' if is_last_node else '│ ')
process_directory_recursively(node_path, base_directory_path, new_prefix_string, ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list)
else:
relative_file_path_string = str(node_path.relative_to(base_directory_path))
file_content_string = read_file_content_safely(node_path)
formatted_file_section = format_file_section(relative_file_path_string, file_content_string)
code_contents_list.append(formatted_file_section)
def execute_bundler_script():
target_output_file_name_string = 'output_fuer_ki.txt'
ignored_directories_list = ['.git', '__pycache__', 'venv', 'env', '.venv', 'node_modules', '.idea', '.vscode', 'instance']
ignored_extensions_list = ['.pyc', '.sqlite', '.db', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.exe', '.pdf', '.zip', '.tar.gz', '.mp3', '.mp4', target_output_file_name_string]
current_working_directory_path = pathlib.Path.cwd()
output_file_path = current_working_directory_path / target_output_file_name_string
tree_lines_list = [current_working_directory_path.name]
code_contents_list = []
process_directory_recursively(current_working_directory_path, current_working_directory_path, '', ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list)
final_output_string = build_final_output_string(tree_lines_list, code_contents_list)
write_output_file(output_file_path, final_output_string)
print('Erfolg! Die Datei ' + target_output_file_name_string + ' wurde generiert.')
if __name__ == '__main__':
execute_bundler_script()
Binary file not shown.
+355 -270
View File
@@ -1,20 +1,207 @@
let websocket_connection; const application_state = {
let pending_administrator_form = null; websocket_connection: null,
let stopwatch_interval_identifier = null; pending_administrator_form: null,
stopwatch_interval_identifier: null
};
function initialize_lucide_icons() { /* --- Deterministische Funktionen ("ehrliche" Funktionen) --- */
if (typeof lucide !== "undefined") {
function calculate_time_difference(start_time_milliseconds, current_time_milliseconds) {
return current_time_milliseconds - start_time_milliseconds;
}
function format_stopwatch_string(difference_in_milliseconds) {
if (difference_in_milliseconds < 0) {
return '00:00 Minuten';
}
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';
}
return output_string;
}
function synchronize_element_nodes(current_node, updated_node) {
if (current_node.nodeType !== updated_node.nodeType) {
current_node.replaceWith(updated_node.cloneNode(true));
return;
}
if (current_node.nodeType === Node.TEXT_NODE) {
if (current_node.nodeValue !== updated_node.nodeValue) {
current_node.nodeValue = updated_node.nodeValue;
}
return;
}
if (current_node.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (current_node.tagName !== updated_node.tagName) {
current_node.replaceWith(updated_node.cloneNode(true));
return;
}
if (current_node.namespaceURI !== updated_node.namespaceURI) {
current_node.replaceWith(updated_node.cloneNode(true));
return;
}
const is_active_input = document.activeElement === current_node;
const is_input_element = ['INPUT', 'TEXTAREA', 'SELECT'].includes(current_node.tagName);
if (is_input_element && !is_active_input) {
if (current_node.value !== updated_node.value) {
current_node.value = updated_node.value;
}
}
const current_attributes = current_node.attributes;
const updated_attributes = updated_node.attributes;
for (let index = 0; index < updated_attributes.length; index++) {
const attribute = updated_attributes[index];
if (current_node.getAttribute(attribute.name) !== attribute.value) {
if (!(is_input_element && is_active_input && attribute.name === 'value')) {
current_node.setAttribute(attribute.name, attribute.value);
}
}
}
for (let index = current_attributes.length - 1; index >= 0; index--) {
const attribute = current_attributes[index];
if (!updated_node.hasAttribute(attribute.name)) {
current_node.removeAttribute(attribute.name);
}
}
const current_children = Array.from(current_node.childNodes);
const updated_children = Array.from(updated_node.childNodes);
const max_length = Math.max(current_children.length, updated_children.length);
for (let index = 0; index < max_length; index++) {
const current_child = current_children[index];
const updated_child = updated_children[index];
if (!current_child && updated_child) {
current_node.appendChild(updated_child.cloneNode(true));
} else if (current_child && !updated_child) {
current_node.removeChild(current_child);
} else if (current_child && updated_child) {
synchronize_element_nodes(current_child, updated_child);
}
}
}
/* --- Architektur & State-Management ("unehrliche" Funktionen) --- */
function trigger_dom_update(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) {
return;
}
const current_active_tab = document.querySelector('.active-tab');
const active_tab_target = current_active_tab ? current_active_tab.getAttribute('data-target') : null;
const active_modals = [];
document.querySelectorAll('.active-modal').forEach(function(modal) {
active_modals.push(modal.id);
});
synchronize_element_nodes(current_container, new_container);
if (active_tab_target) {
document.querySelectorAll('.tab-button').forEach(function(button) { button.classList.remove('active-tab'); });
document.querySelectorAll('.tab-content').forEach(function(content) { content.classList.remove('active-content'); });
const restored_tab = document.querySelector('.tab-button[data-target="' + active_tab_target + '"]');
const restored_content = document.getElementById(active_tab_target);
if (restored_tab && restored_content) {
restored_tab.classList.add('active-tab');
restored_content.classList.add('active-content');
}
}
active_modals.forEach(function(modal_identifier) {
const restored_modal = document.getElementById(modal_identifier);
if (restored_modal) {
restored_modal.classList.add('active-modal');
}
});
if (typeof lucide !== 'undefined') {
lucide.createIcons(); lucide.createIcons();
} }
} }
function update_game_stopwatch() { function fetch_and_synchronize_dom() {
const stopwatch_element = document.getElementById("game-stopwatch"); fetch(window.location.href)
.then(function(network_response) {
if (!network_response.ok) {
throw new Error('Network response failure');
}
return network_response.text();
})
.then(function(markup_text) {
trigger_dom_update(markup_text);
})
.catch(function(error_object) {
console.error('DOM Synchronization failed:', error_object);
});
}
function execute_asynchronous_form_submission(target_form) {
fetch(target_form.action, {
method: target_form.method,
body: new FormData(target_form)
})
.then(function(network_response) {
if (!network_response.ok) {
throw new Error('Form submission failure');
}
return network_response.text();
})
.then(function(markup_text) {
trigger_dom_update(markup_text);
})
.catch(function(error_object) {
console.error('Form execution failed:', error_object);
});
}
function update_game_stopwatch_ui() {
const stopwatch_element = document.getElementById('game-stopwatch');
if (!stopwatch_element) { if (!stopwatch_element) {
return; return;
} }
const start_time_string = stopwatch_element.getAttribute("data-start-time"); const start_time_string = stopwatch_element.getAttribute('data-start-time');
if (!start_time_string) { if (!start_time_string) {
return; return;
} }
@@ -25,306 +212,204 @@ function update_game_stopwatch() {
} }
const current_date = new Date(); const current_date = new Date();
const difference_in_milliseconds = current_date.getTime() - start_date.getTime(); const difference = calculate_time_difference(start_date.getTime(), current_date.getTime());
stopwatch_element.textContent = format_stopwatch_string(difference);
}
if (difference_in_milliseconds < 0) { function modify_modal_state(modal_identifier, is_active) {
stopwatch_element.textContent = "00:00 Minuten"; const modal = document.getElementById(modal_identifier);
if (modal) {
if (is_active) {
modal.classList.add('active-modal');
} else {
modal.classList.remove('active-modal');
}
}
}
function write_text_to_clipboard(text_payload) {
if (!navigator.clipboard) {
console.error('Clipboard API unvailable.');
return;
}
navigator.clipboard.writeText(text_payload).catch(function(error_object) {
console.error('Clipboard action failed:', error_object);
});
}
/* --- Globale Event-Delegation --- */
function handle_global_clicks(mouse_event) {
const target_element = mouse_event.target;
const tab_button = target_element.closest('.tab-button');
if (tab_button) {
const target_identifier = tab_button.getAttribute('data-target');
const parent_container = tab_button.closest('.application-content');
parent_container.querySelectorAll('.tab-button').forEach(function(button) {
button.classList.remove('active-tab');
});
parent_container.querySelectorAll('.tab-content').forEach(function(content) {
content.classList.remove('active-content');
});
tab_button.classList.add('active-tab');
const target_panel = document.getElementById(target_identifier);
if (target_panel) {
target_panel.classList.add('active-content');
}
return; return;
} }
const total_seconds = Math.floor(difference_in_milliseconds / 1000); const action_mapping = {
const days_passed = Math.floor(total_seconds / 86400); 'trigger-elimination': function() { modify_modal_state('elimination-confirm-modal', true); },
const hours_passed = Math.floor((total_seconds % 86400) / 3600); 'cancel-elimination': function() { modify_modal_state('elimination-confirm-modal', false); },
const minutes_passed = Math.floor((total_seconds % 3600) / 60); 'proceed-elimination': function() {
const seconds_passed = total_seconds % 60; if (application_state.websocket_connection) {
application_state.websocket_connection.emit('request_elimination');
let output_string = "";
if (days_passed === 1) {
output_string += "1 Tag und ";
} else if (days_passed > 1) {
output_string += days_passed + " Tage und ";
} }
modify_modal_state('elimination-confirm-modal', false);
const padded_minutes = String(minutes_passed).padStart(2, "0"); },
const padded_seconds = String(seconds_passed).padStart(2, "0"); 'trigger-revoke-elimination': function() {
if (application_state.websocket_connection) {
if (hours_passed === 1) { application_state.websocket_connection.emit('revoke_elimination_request');
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; 'confirm-elimination': function() {
} if (application_state.websocket_connection) {
application_state.websocket_connection.emit('confirm_elimination');
function initialize_application_behavior() {
initialize_lucide_icons();
if (stopwatch_interval_identifier) {
clearInterval(stopwatch_interval_identifier);
} }
stopwatch_interval_identifier = setInterval(update_game_stopwatch, 1000); modify_modal_state('verification-modal', false);
update_game_stopwatch(); },
'reject-elimination': function() {
const tab_buttons = document.querySelectorAll(".tab-button"); if (application_state.websocket_connection) {
tab_buttons.forEach(function(current_button) { application_state.websocket_connection.emit('reject_elimination');
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");
} }
}); modify_modal_state('verification-modal', false);
}); },
'acknowledge-rejection': function() { modify_modal_state('rejection-modal', false); },
const player_forms = document.querySelectorAll(".asynchronous-form"); 'trigger-name-change-modal': function() { modify_modal_state('name-change-modal', true); },
player_forms.forEach(function(current_form) { 'cancel-name-change-modal': function() { modify_modal_state('name-change-modal', false); },
current_form.addEventListener("submit", function(form_submission_event) { 'trigger-help-modal': function() { modify_modal_state('help-modal', true); },
form_submission_event.preventDefault(); 'close-help-modal': function() { modify_modal_state('help-modal', false); },
execute_form_submission(current_form); 'copy-identifier-button': function(button_element) {
}); const identifier = button_element.getAttribute('data-identifier');
}); write_text_to_clipboard(identifier);
const icon = button_element.querySelector('i, svg');
const administrator_forms = document.querySelectorAll(".admin-asynchronous-form"); if (icon) {
administrator_forms.forEach(function(current_form) { icon.setAttribute('data-lucide', 'check');
current_form.addEventListener("submit", function(form_submission_event) { if (typeof lucide !== 'undefined') {
form_submission_event.preventDefault(); lucide.createIcons();
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");
} }
}); },
}); 'confirm-admin-action': function() {
if (application_state.pending_administrator_form) {
const confirm_administrator_button = document.getElementById("confirm-admin-action"); const time_input = document.getElementById('modal_execution_time');
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) { if (time_input && time_input.value) {
let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); let hidden_input = application_state.pending_administrator_form.querySelector('input[name="execution_time"]');
if (!hidden_time_input) { if (!hidden_input) {
hidden_time_input = document.createElement("input"); hidden_input = document.createElement('input');
hidden_time_input.type = "hidden"; hidden_input.type = 'hidden';
hidden_time_input.name = "execution_time"; hidden_input.name = 'execution_time';
pending_administrator_form.appendChild(hidden_time_input); application_state.pending_administrator_form.appendChild(hidden_input);
} }
hidden_time_input.value = time_input.value; hidden_input.value = time_input.value;
} else { } else {
let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); const hidden_input = application_state.pending_administrator_form.querySelector('input[name="execution_time"]');
if (hidden_time_input) { if (hidden_input) {
hidden_time_input.remove(); hidden_input.remove();
} }
} }
execute_form_submission(pending_administrator_form); execute_asynchronous_form_submission(application_state.pending_administrator_form);
pending_administrator_form = null; application_state.pending_administrator_form = null;
const administrator_modal = document.getElementById("admin-confirmation-modal"); modify_modal_state('admin-confirmation-modal', false);
if (administrator_modal) {
administrator_modal.classList.remove("active-modal");
} }
},
'cancel-admin-action': function() {
application_state.pending_administrator_form = null;
modify_modal_state('admin-confirmation-modal', false);
} }
}); };
}
const cancel_administrator_button = document.getElementById("cancel-admin-action"); for (const [identifier, action_function] of Object.entries(action_mapping)) {
if (cancel_administrator_button) { const button = target_element.closest('#' + identifier);
cancel_administrator_button.addEventListener("click", function() { if (button) {
pending_administrator_form = null; action_function(button);
const administrator_modal = document.getElementById("admin-confirmation-modal"); return;
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) { function handle_global_submits(submit_event) {
const active_tab_element = document.querySelector(".active-tab"); const target_form = submit_event.target;
let active_target = "control-panel";
if (active_tab_element) {
active_target = active_tab_element.getAttribute("data-target");
}
fetch(target_form.action, { if (target_form.classList.contains('asynchronous-form')) {
method: target_form.method, submit_event.preventDefault();
body: new FormData(target_form) execute_asynchronous_form_submission(target_form);
}) } else if (target_form.classList.contains('admin-asynchronous-form')) {
.then(function(network_response) { return network_response.text(); }) submit_event.preventDefault();
.then(function(markup_text) { application_state.pending_administrator_form = target_form;
replace_main_content(markup_text); const time_input = document.getElementById('modal_execution_time');
const new_tab_button = document.querySelector(".tab-button[data-target='" + active_target + "']"); if (time_input) {
if (new_tab_button) { time_input.value = '';
new_tab_button.click();
} }
}); modify_modal_state('admin-confirmation-modal', true);
}
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() { /* --- Initialisierung --- */
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service_worker.js"); document.addEventListener('DOMContentLoaded', function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service_worker.js').catch(function(error_object) {
console.error('Service Worker Registration failed:', error_object);
});
} }
if (typeof io !== "undefined") { if (typeof lucide !== 'undefined') {
websocket_connection = io(); lucide.createIcons();
}
websocket_connection.on("game_state_changed", function() { if (application_state.stopwatch_interval_identifier) {
if (document.querySelector(".game-section") || document.querySelector(".administrator-section")) { clearInterval(application_state.stopwatch_interval_identifier);
fetch(window.location.href) }
.then(function(network_response) { return network_response.text(); }) application_state.stopwatch_interval_identifier = setInterval(update_game_stopwatch_ui, 1000);
.then(function(markup_text) { update_game_stopwatch_ui();
replace_main_content(markup_text);
}); document.addEventListener('click', handle_global_clicks);
document.addEventListener('submit', handle_global_submits);
if (typeof io !== 'undefined') {
application_state.websocket_connection = io();
application_state.websocket_connection.on('game_state_changed', function() {
if (document.querySelector('.game-section') || document.querySelector('.administrator-section')) {
fetch_and_synchronize_dom();
} }
}); });
websocket_connection.on("admin_state_changed", function() { application_state.websocket_connection.on('admin_state_changed', function() {
if (document.querySelector(".administrator-section")) { if (document.querySelector('.administrator-section')) {
fetch(window.location.href) fetch_and_synchronize_dom();
.then(function(network_response) { return network_response.text(); })
.then(function(markup_text) {
replace_main_content(markup_text);
});
} }
}); });
websocket_connection.on("elimination_verification_requested", function() { application_state.websocket_connection.on('elimination_verification_requested', function() {
fetch(window.location.href) fetch_and_synchronize_dom();
.then(function(network_response) { return network_response.text(); }) setTimeout(function() {
.then(function(markup_text) { modify_modal_state('verification-modal', true);
replace_main_content(markup_text); }, 50);
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() { application_state.websocket_connection.on('elimination_rejected', function() {
const waiting_modal = document.getElementById("waiting-modal"); modify_modal_state('rejection-modal', true);
if (waiting_modal) { });
waiting_modal.classList.remove("active-modal");
} application_state.websocket_connection.on('elimination_revoked', function() {
const rejection_modal = document.getElementById("rejection-modal"); modify_modal_state('verification-modal', false);
if (rejection_modal) { fetch_and_synchronize_dom();
rejection_modal.classList.add("active-modal");
}
}); });
} }
initialize_application_behavior();
}); });
+9 -2
View File
@@ -1,2 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="465 137 354 385" width="354" height="385">
<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> <style>
path {
fill: hsl(60 60% 60%);
fill: oklch(85% 0.15 110);
}
</style>
<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"/>
</svg>

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 718 B

+136 -27
View File
@@ -1,9 +1,25 @@
:root { :root {
--fallback-primary: hsl(15, 80%, 45%);
--fallback-background: hsl(15, 20%, 95%);
--fallback-surface: hsl(0, 0%, 100%);
--fallback-text: hsl(250, 20%, 15%);
--fallback-static-white: hsl(0, 0%, 100%);
--fallback-static-black: hsl(250, 20%, 15%);
--fallback-text-inverted: hsl(0, 0%, 100%);
--fallback-success: hsl(140, 50%, 45%);
--fallback-danger: hsl(25, 80%, 50%);
--fallback-warning: hsl(50, 90%, 65%);
--fallback-border: hsla(250, 20%, 15%, 0.2);
--fallback-border-light: hsla(250, 20%, 15%, 0.1);
--fallback-primary-transparent: hsla(15, 80%, 45%, 0.1);
--fallback-shadow-primary: hsla(15, 80%, 45%, 0.3);
--fallback-overlay: rgba(0, 0, 0, 0.6);
--lightness-primary: 0.35; --lightness-primary: 0.35;
--chroma-primary: 0.15; --chroma-primary: 0.15;
--hue-primary: 15; --hue-primary: 15;
--lightness-background: 0.9; --lightness-background: 0.95;
--chroma-background: 0.01; --chroma-background: 0.01;
--hue-background: var(--hue-primary); --hue-background: var(--hue-primary);
@@ -15,6 +31,18 @@
--chroma-text: 0.05; --chroma-text: 0.05;
--hue-text: 250; --hue-text: 250;
--lightness-static-white: 1;
--chroma-static-white: 0;
--hue-static-white: 0;
--lightness-static-black: 0.15;
--chroma-static-black: 0.05;
--hue-static-black: 250;
--lightness-text-inverted: 1;
--chroma-text-inverted: 0;
--hue-text-inverted: 0;
--lightness-success: 0.60; --lightness-success: 0.60;
--chroma-success: 0.18; --chroma-success: 0.18;
--hue-success: 140; --hue-success: 140;
@@ -23,14 +51,15 @@
--chroma-danger: 0.22; --chroma-danger: 0.22;
--hue-danger: 25; --hue-danger: 25;
--lightness-warning: 0.75; --lightness-warning: 0.85;
--chroma-warning: 0.18; --chroma-warning: 0.15;
--hue-warning: 70; --hue-warning: 80;
--dimension-multiplier: 1; --dimension-multiplier: 1;
--spacing-small: calc(0.5rem * var(--dimension-multiplier)); --spacing-scale-base: 0.25rem;
--spacing-medium: calc(1rem * var(--dimension-multiplier)); --spacing-small: calc(var(--spacing-scale-base) * 2 * var(--dimension-multiplier));
--spacing-large: calc(1.5rem * var(--dimension-multiplier)); --spacing-medium: calc(var(--spacing-scale-base) * 4 * var(--dimension-multiplier));
--spacing-large: calc(var(--spacing-scale-base) * 6 * var(--dimension-multiplier));
--border-radius-small: calc(0.5rem * var(--dimension-multiplier)); --border-radius-small: calc(0.5rem * var(--dimension-multiplier));
--border-radius-large: calc(1.25rem * var(--dimension-multiplier)); --border-radius-large: calc(1.25rem * var(--dimension-multiplier));
@@ -39,17 +68,31 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--fallback-primary: hsl(15, 80%, 60%);
--fallback-background: hsl(250, 15%, 15%);
--fallback-surface: hsl(250, 15%, 20%);
--fallback-text: hsl(250, 20%, 90%);
--fallback-text-inverted: hsl(0, 0%, 0%);
--fallback-border: hsla(250, 20%, 90%, 0.2);
--fallback-border-light: hsla(250, 20%, 90%, 0.1);
--fallback-primary-transparent: hsla(15, 80%, 60%, 0.1);
--fallback-shadow-primary: hsla(15, 80%, 60%, 0.3);
--lightness-primary: 0.65; --lightness-primary: 0.65;
--chroma-primary: 0.20; --chroma-primary: 0.20;
--lightness-background: 0.2; --lightness-background: 0.15;
--chroma-background: 0.02; --chroma-background: 0.02;
--lightness-surface: 0.18; --lightness-surface: 0.20;
--chroma-surface: 0.02; --chroma-surface: 0.02;
--lightness-text: 0.90; --lightness-text: 0.90;
--chroma-text: 0.02; --chroma-text: 0.02;
--lightness-text-inverted: 0;
--chroma-text-inverted: 0;
--hue-text-inverted: 0;
} }
} }
@@ -61,7 +104,9 @@
} }
body { body {
background-color: var(--fallback-background);
background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background));
color: var(--fallback-text);
color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text));
font-family: system-ui, -apple-system, sans-serif; font-family: system-ui, -apple-system, sans-serif;
font-size: calc(1rem * var(--dimension-multiplier)); font-size: calc(1rem * var(--dimension-multiplier));
@@ -71,16 +116,31 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
.brand-link {
text-decoration: none;
color: inherit;
display: block;
transition: opacity 0.2s ease;
}
.brand-link:hover {
opacity: 0.8;
} }
.application-header { .application-header {
background-color: var(--fallback-primary);
background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
color: oklch(1 0 0); color: var(--fallback-text-inverted);
color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted));
padding: var(--spacing-medium) var(--spacing-large); padding: var(--spacing-medium) var(--spacing-large);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px var(--fallback-border-light);
box-shadow: 0 4px 12px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
@@ -115,11 +175,14 @@ body {
} }
.content-card { .content-card {
background-color: var(--fallback-surface);
background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
padding: var(--spacing-large); padding: var(--spacing-large);
border-radius: var(--border-radius-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); box-shadow: 0 10px 25px -5px var(--fallback-border-light), 0 8px 10px -6px var(--fallback-border-light);
box-shadow: 0 10px 25px -5px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.05), 0 8px 10px -6px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.01);
margin-bottom: var(--spacing-large); margin-bottom: var(--spacing-large);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
} }
.section-title { .section-title {
@@ -169,16 +232,19 @@ body {
.text-input { .text-input {
padding: var(--spacing-medium); padding: var(--spacing-medium);
border: 2px solid oklch(0.5 0 0 / 0.2); border: 2px solid var(--fallback-border);
border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: var(--fallback-background);
background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background));
color: inherit; color: inherit;
font-size: inherit; font-size: inherit;
transition: border-color 0.2s; transition: border-color 0.2s ease, background-color 0.3s ease;
} }
.text-input:focus { .text-input:focus {
outline: none; outline: none;
border-color: var(--fallback-primary);
border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
} }
@@ -203,7 +269,8 @@ body {
justify-content: center; justify-content: center;
gap: var(--spacing-small); gap: var(--spacing-small);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: oklch(1 0 0); color: var(--fallback-text-inverted);
color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted));
} }
.action-button:hover, .action-button:focus { .action-button:hover, .action-button:focus {
@@ -217,26 +284,34 @@ body {
} }
.primary-button { .primary-button {
background-color: var(--fallback-primary);
background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
box-shadow: 0 4px 12px var(--fallback-shadow-primary);
box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3); box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3);
} }
.success-button { .success-button {
background-color: var(--fallback-success);
background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
} }
.danger-button { .danger-button {
background-color: var(--fallback-danger);
background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
} }
.warning-button { .warning-button {
background-color: var(--fallback-warning);
background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
color: oklch(0.2 0 0); color: var(--fallback-static-black);
color: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black));
} }
.secondary-button { .secondary-button {
background-color: transparent; background-color: transparent;
color: var(--fallback-text);
color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text));
border: 2px solid var(--fallback-border);
border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2); border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2);
} }
@@ -253,11 +328,13 @@ body {
.tab-container { .tab-container {
display: flex; display: flex;
background-color: var(--fallback-surface);
background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
border-radius: var(--border-radius-pill); border-radius: var(--border-radius-pill);
padding: calc(0.375rem * var(--dimension-multiplier)); padding: calc(0.375rem * var(--dimension-multiplier));
gap: calc(0.25rem * var(--dimension-multiplier)); gap: calc(0.25rem * var(--dimension-multiplier));
box-shadow: 0 4px 12px rgba(0,0,0,0.05); box-shadow: 0 4px 12px var(--fallback-border-light);
box-shadow: 0 4px 12px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.05);
margin-bottom: var(--spacing-large); margin-bottom: var(--spacing-large);
overflow-x: auto; overflow-x: auto;
} }
@@ -281,9 +358,12 @@ body {
} }
.active-tab { .active-tab {
background-color: var(--fallback-primary);
background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
color: oklch(1 0 0); color: var(--fallback-text-inverted);
box-shadow: 0 2px 8px rgba(0,0,0,0.15); color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted));
box-shadow: 0 2px 8px var(--fallback-border-light);
box-shadow: 0 2px 8px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.15);
} }
.tab-content { .tab-content {
@@ -292,6 +372,12 @@ body {
.active-content { .active-content {
display: block; display: block;
animation: fade-in 0.3s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(calc(0.25rem * var(--dimension-multiplier))); }
to { opacity: 1; transform: translateY(0); }
} }
.status-pill { .status-pill {
@@ -302,36 +388,45 @@ body {
border-radius: var(--border-radius-pill); border-radius: var(--border-radius-pill);
font-size: calc(0.875rem * var(--dimension-multiplier)); font-size: calc(0.875rem * var(--dimension-multiplier));
font-weight: bold; font-weight: bold;
color: oklch(1 0 0); color: var(--fallback-static-white);
color: oklch(var(--lightness-static-white) var(--chroma-static-white) var(--hue-static-white));
margin-left: var(--spacing-small); margin-left: var(--spacing-small);
} }
.warning-pill { .warning-pill {
background-color: var(--fallback-warning);
background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
color: oklch(0.2 0 0); color: var(--fallback-static-black);
color: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black));
} }
.success-pill { .success-pill {
background-color: var(--fallback-success);
background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
} }
.danger-pill { .danger-pill {
background-color: var(--fallback-danger);
background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
} }
.notice-card { .notice-card {
border-left: 6px solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); border-left: calc(0.375rem * var(--dimension-multiplier)) solid var(--fallback-primary);
border-left: calc(0.375rem * var(--dimension-multiplier)) solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
} }
.danger-card { .danger-card {
border-left-color: var(--fallback-danger);
border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));
} }
.success-card { .success-card {
border-left-color: var(--fallback-success);
border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success));
} }
.warning-card { .warning-card {
border-left-color: var(--fallback-warning);
border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning));
} }
@@ -339,6 +434,7 @@ body {
font-size: calc(2.5rem * var(--dimension-multiplier)); font-size: calc(2.5rem * var(--dimension-multiplier));
text-align: center; text-align: center;
margin: var(--spacing-large) 0; margin: var(--spacing-large) 0;
color: var(--fallback-primary);
color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
font-weight: 800; font-weight: 800;
} }
@@ -350,7 +446,8 @@ body {
.data-table th, .data-table td { .data-table th, .data-table td {
padding: var(--spacing-medium); padding: var(--spacing-medium);
border-bottom: 1px solid oklch(0.5 0 0 / 0.1); border-bottom: 1px solid var(--fallback-border-light);
border-bottom: 1px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1);
text-align: left; text-align: left;
} }
@@ -363,6 +460,7 @@ body {
} }
.highlighted-row { .highlighted-row {
background-color: var(--fallback-primary-transparent);
background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1); background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1);
} }
@@ -375,6 +473,7 @@ body {
} }
.markdown-container h1, .markdown-container h2, .markdown-container h3 { .markdown-container h1, .markdown-container h2, .markdown-container h3 {
color: var(--fallback-primary);
color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary));
} }
@@ -389,7 +488,8 @@ body {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0,0,0,0.6); background-color: var(--fallback-overlay);
background-color: oklch(0 0 0 / 0.6);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -401,12 +501,14 @@ body {
} }
.modal-dialog { .modal-dialog {
background-color: var(--fallback-surface);
background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface));
padding: var(--spacing-large); padding: var(--spacing-large);
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
max-width: calc(32rem * var(--dimension-multiplier)); max-width: calc(32rem * var(--dimension-multiplier));
width: 90%; width: 90%;
box-shadow: 0 20px 40px rgba(0,0,0,0.2); box-shadow: 0 20px 40px var(--fallback-border);
box-shadow: 0 20px 40px oklch(0 0 0 / 0.2);
} }
.modal-title { .modal-title {
@@ -424,12 +526,14 @@ body {
} }
.qr-code-container { .qr-code-container {
background-color: oklch(1 0 0); background-color: var(--fallback-static-white);
background-color: oklch(var(--lightness-static-white) var(--chroma-static-white) var(--hue-static-white));
padding: var(--spacing-medium); padding: var(--spacing-medium);
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
display: inline-block; display: inline-block;
margin-bottom: var(--spacing-medium); margin-bottom: var(--spacing-medium);
box-shadow: inset 0 0 0 2px oklch(0.5 0 0 / 0.1); box-shadow: inset 0 0 0 2px var(--fallback-border-light);
box-shadow: inset 0 0 0 2px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1);
} }
.qr-code-container svg { .qr-code-container svg {
@@ -438,6 +542,11 @@ body {
height: calc(14rem * var(--dimension-multiplier)); height: calc(14rem * var(--dimension-multiplier));
} }
.qr-code-container svg path {
fill: var(--fallback-static-black);
fill: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black));
}
.monospace-identifier { .monospace-identifier {
font-family: monospace; font-family: monospace;
} }
+22 -22
View File
@@ -3,22 +3,22 @@
<section class="administrator-section"> <section class="administrator-section">
{% if setup_required %} {% if setup_required %}
<div class="content-card"> <div class="content-card">
<h2 class="section-title"><i data-lucide="settings"></i> Administrator Einrichtung</h2> <h2 class="section-title"><i data-lucide="settings"></i> Einrichtung</h2>
<form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form"> <form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form">
<div class="form-group"> <div class="form-group">
<label for="password_string" class="input-label">Neues Administrator-Passwort</label> <label for="password_string" class="input-label">Neues Passwort</label>
<input type="password" id="password_string" name="password_string" class="text-input" required> <input type="password" id="password_string" name="password_string" class="text-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label> <label for="password_string_confirmation" class="input-label">Passwort bestätigen</label>
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required> <input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
</div> </div>
<button type="submit" class="action-button primary-button"><i data-lucide="check"></i> Einrichtung abschließen</button> <button type="submit" class="action-button primary-button"><i data-lucide="check"></i> Abschließen</button>
</form> </form>
</div> </div>
{% elif login_required %} {% elif login_required %}
<div class="content-card"> <div class="content-card">
<h2 class="section-title"><i data-lucide="shield"></i> Administrator Anmeldung</h2> <h2 class="section-title"><i data-lucide="shield"></i> Admin-Login</h2>
<form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form"> <form action="{{ url_for('handle_administrator') }}" method="POST" class="input-form">
<div class="form-group"> <div class="form-group">
<label for="password_string" class="input-label">Passwort</label> <label for="password_string" class="input-label">Passwort</label>
@@ -26,7 +26,7 @@
</div> </div>
{% if two_factor_enabled %} {% if two_factor_enabled %}
<div class="form-group"> <div class="form-group">
<label for="two_factor_code" class="input-label">Zwei-Faktor Code</label> <label for="two_factor_code" class="input-label">2FA-Code</label>
<input type="text" id="two_factor_code" name="two_factor_code" class="text-input" required> <input type="text" id="two_factor_code" name="two_factor_code" class="text-input" required>
</div> </div>
{% endif %} {% endif %}
@@ -63,7 +63,7 @@
</form> </form>
{% else %} {% else %}
<div class="content-card notice-card warning-card"> <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> <p class="notice-text">Zu wenige Spieler zum Starten.</p>
</div> </div>
{% endif %} {% endif %}
{% elif game_status == 'running' %} {% elif game_status == 'running' %}
@@ -97,7 +97,7 @@
{% if two_factor_enabled %} {% if two_factor_enabled %}
<div class="content-card notice-card success-card"> <div class="content-card notice-card success-card">
<p class="notice-text">Zwei-Faktor-Authentifizierung ist aktiv.</p> <p class="notice-text">2FA ist aktiv.</p>
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="inline-form admin-asynchronous-form"> <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"> <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> <button type="submit" class="action-button danger-button"><i data-lucide="shield-off"></i> Deaktivieren</button>
@@ -105,8 +105,8 @@
</div> </div>
{% else %} {% else %}
<div class="content-card notice-card warning-card"> <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 class="notice-text">2FA inaktiv. QR-Code scannen.</p>
<p>Manuelle Eingabe: <strong>{{ two_factor_secret }}</strong></p> <p>Code: <strong>{{ two_factor_secret }}</strong></p>
<div class="qr-code-container"> <div class="qr-code-container">
{{ qr_code_svg | safe }} {{ qr_code_svg | safe }}
</div> </div>
@@ -124,28 +124,28 @@
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form" style="margin-top: var(--spacing-large);"> <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"> <input type="hidden" name="action_type" value="update_credentials">
<div class="form-group"> <div class="form-group">
<label for="new_password_string" class="input-label">Passwort ändern</label> <label for="new_password_string" class="input-label">Neues Passwort</label>
<input type="password" id="new_password_string" name="password_string" class="text-input" required> <input type="password" id="new_password_string" name="password_string" class="text-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="update_password_confirmation" class="input-label">Passwort wiederholen</label> <label for="update_password_confirmation" class="input-label">Passwort bestätigen</label>
<input type="password" id="update_password_confirmation" name="password_string_confirmation" class="text-input" required> <input type="password" id="update_password_confirmation" name="password_string_confirmation" class="text-input" required>
</div> </div>
<button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Aktualisieren</button> <button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Speichern</button>
</form> </form>
</div> </div>
</div> </div>
<div id="player-panel" class="tab-content"> <div id="player-panel" class="tab-content">
<div class="content-card"> <div class="content-card">
<h3 class="card-title"><i data-lucide="user-plus"></i> Spieler manuell erstellen</h3> <h3 class="card-title"><i data-lucide="user-plus"></i> Spieler erstellen</h3>
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form"> <form action="{{ url_for('handle_administrator_action') }}" method="POST" class="input-form admin-asynchronous-form">
<input type="hidden" name="action_type" value="create_player"> <input type="hidden" name="action_type" value="create_player">
<div class="form-group"> <div class="form-group">
<label for="display_name" class="input-label">Anzeigename</label> <label for="display_name" class="input-label">Name</label>
<input type="text" id="display_name" name="display_name" class="text-input" required> <input type="text" id="display_name" name="display_name" class="text-input" required>
</div> </div>
<button type="submit" class="action-button primary-button"><i data-lucide="plus"></i> Spieler erstellen</button> <button type="submit" class="action-button primary-button"><i data-lucide="plus"></i> Erstellen</button>
</form> </form>
</div> </div>
@@ -154,7 +154,7 @@
<thead> <thead>
<tr> <tr>
<th>Kennung</th> <th>Kennung</th>
<th>Name & Änderung</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>Ziel</th> <th>Ziel</th>
<th>Aktionen</th> <th>Aktionen</th>
@@ -181,7 +181,7 @@
{% if current_player.pending_display_name %} {% 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;"> <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));"> <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> -> <strong>{{ current_player.pending_display_name }}</strong>
</p> </p>
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-bottom: var(--spacing-small);"> <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="action_type" value="approve_name_change">
@@ -209,10 +209,10 @@
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);"> <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="action_type" value="eliminate_player">
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}"> <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> <button type="submit" class="action-button danger-button small-button"><i data-lucide="skull"></i> Killen</button>
</form> </form>
{% else %} {% else %}
<span class="status-pill danger-pill" style="margin-left: 0;"><i data-lucide="skull"></i> Tot</span> <span class="status-pill danger-pill" style="margin-left: 0;"><i data-lucide="skull"></i> Gekillt</span>
<form action="{{ url_for('handle_administrator_action') }}" method="POST" class="flexible_form_container admin-asynchronous-form" style="margin-top: var(--spacing-small);"> <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="action_type" value="revive_player">
<input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}"> <input type="hidden" name="player_identifier" value="{{ current_player.player_identifier }}">
@@ -321,7 +321,7 @@
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="5" style="text-align: center; opacity: 0.6;">Bisher keine Meldungen im System.</td> <td colspan="5" style="text-align: center; opacity: 0.6;">Keine Meldungen.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -332,7 +332,7 @@
<div id="admin-confirmation-modal" class="modal-overlay"> <div id="admin-confirmation-modal" class="modal-overlay">
<div class="modal-dialog"> <div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="calendar-clock"></i> Aktion planen</h2> <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> <p class="modal-description">Sofort ausführen oder planen.</p>
<div class="form-group" style="margin-bottom: var(--spacing-large); text-align: left;"> <div class="form-group" style="margin-bottom: var(--spacing-large); text-align: left;">
<label for="modal_execution_time" class="input-label">Zeitpunkt (Optional)</label> <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;"> <input type="datetime-local" id="modal_execution_time" class="text-input time-input" style="width: 100%; box-sizing: border-box;">
+46 -39
View File
@@ -2,20 +2,21 @@
{% block content %} {% block content %}
<section class="game-section"> <section class="game-section">
<div class="content-card"> <div class="content-card">
<h2 class="section-title" style="margin: 0;"><i data-lucide="user"></i> Willkommen, {{ player.display_name }} ({{ player.player_identifier }})!</h2> <h2 class="section-title" style="margin: 0;"><i data-lucide="user"></i> Hallo, {{ player.display_name }}!</h2>
</div> </div>
{% if not player.is_approved %} {% if not player.is_approved %}
<div class="content-card notice-card warning-card"> <div class="content-card notice-card warning-card">
<h2 class="modal-title"><i data-lucide="hourglass"></i> Konto wird geprüft&hellip;</h2> <h2 class="modal-title"><i data-lucide="hourglass"></i> Warten auf Freigabe</h2>
<p class="notice-text">Die Spielleitung muss deinen Account noch aktivieren.</p>
</div> </div>
{% elif game_status == 'pending' %} {% elif game_status == 'pending' %}
<div class="content-card notice-card"> <div class="content-card notice-card">
<h2 class="modal-title"><i data-lucide="clock"></i> Startet bald&hellip;</h2> <h2 class="modal-title"><i data-lucide="clock"></i> Spiel startet in Kürze</h2>
</div> </div>
{% elif game_status == 'finished' %} {% elif game_status == 'finished' %}
<div class="content-card leaderboard-card"> <div class="content-card leaderboard-card">
<h2 class="section-title"><i data-lucide="trophy"></i> Spiel beendet &ndash; Rangliste</h2> <h2 class="section-title"><i data-lucide="trophy"></i> Rangliste</h2>
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
@@ -45,45 +46,58 @@
<div id="mission-panel" class="tab-content active-content"> <div id="mission-panel" class="tab-content active-content">
{% if game_status == 'paused' %} {% if game_status == 'paused' %}
<div class="content-card notice-card warning-card"> <div class="content-card notice-card warning-card">
<h2 class="modal-title"><i data-lucide="pause-circle"></i> Spiel pausiert</h2> <h2 class="modal-title"><i data-lucide="pause-circle"></i> Pausiert</h2>
<p class="notice-text">Das Spiel wurde pausiert. Eliminierungen sind aktuell nicht möglich.</p> <p class="notice-text">Kills sind vorübergehend deaktiviert.</p>
</div> </div>
{% endif %} {% endif %}
{% if player.is_alive %} {% if player.is_alive %}
<div class="content-card target-card" style="text-align: center;"> <div class="content-card target-card" style="text-align: center;">
<h3 class="card-subtitle"><i data-lucide="target"></i> Dein Ziel</h3> <h3 class="card-subtitle"><i data-lucide="target"></i> Ziel</h3>
<h2 class="target-name">{{ target_player.display_name if target_player else 'Niemand' }}</h2> <h2 class="target-name">{{ target_player.display_name if target_player else 'Niemand' }}</h2>
{% if game_status == 'running' %} {% 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> {% if has_pending_kill_report %}
<div class="notice-card warning-card" style="padding: var(--spacing-medium); border-radius: var(--border-radius-small); margin-top: var(--spacing-large);">
<h3 style="margin-top: 0; display: flex; align-items: center; justify-content: center; gap: var(--spacing-small);"><i data-lucide="loader"></i> Kill gemeldet</h3>
<p>Warte auf Bestätigung…</p>
<button type="button" id="trigger-revoke-elimination" class="action-button secondary-button"><i data-lucide="rotate-ccw"></i> Zurückziehen</button>
</div>
{% else %} {% else %}
<button type="button" class="action-button secondary-button large-button" disabled><i data-lucide="lock"></i> Pausiert</button> <button type="button" id="trigger-elimination" class="action-button danger-button large-button"><i data-lucide="crosshair"></i> Kill melden</button>
{% endif %}
{% else %}
<button type="button" class="action-button secondary-button large-button" disabled><i data-lucide="lock"></i> Gesperrt</button>
{% endif %} {% endif %}
</div> </div>
{% elif player.late_joiner %} {% elif player.late_joiner %}
<div class="content-card notice-card warning-card"> <div class="content-card notice-card warning-card">
<h2 class="modal-title"><i data-lucide="users"></i> Warte auf Einstieg&hellip;</h2> <h2 class="modal-title"><i data-lucide="users"></i> Warten auf Einwechslung</h2>
<p class="notice-text">Du bist dem Spiel während einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.</p> <p class="notice-text">Die Spielleitung wechselt dich bald ein.</p>
</div> </div>
{% else %} {% else %}
<div class="content-card notice-card danger-card"> <div class="content-card notice-card danger-card">
<h2 class="modal-title"><i data-lucide="skull"></i> Eliminiert</h2> <h2 class="modal-title"><i data-lucide="skull"></i> Gekillt</h2>
</div> </div>
{% endif %} {% endif %}
<div class="content-card statistics-card"> <div class="content-card statistics-card">
<h3 class="section-title"><i data-lucide="pie-chart"></i> Spielübersicht</h3> <h3 class="section-title"><i data-lucide="pie-chart"></i> Übersicht</h3>
<p>Überlebende: <strong>{{ alive_count }}</strong></p> <p>Lebend: <strong>{{ alive_count }}</strong></p>
<p>Deine Kills: <strong>{{ player.score }}</strong></p> <p>Kills: <strong>{{ player.score }}</strong></p>
<p>Gesamte Kills: <strong>{{ total_eliminations }}</strong></p> <p>Gesamt: <strong>{{ total_eliminations }}</strong></p>
<p>Laufzeit: <strong id="game-stopwatch" data-start-time="{{ start_time }}">Wird berechnet...</strong></p> <p>Zeit: <strong id="game-stopwatch" data-start-time="{{ start_time }}">Lade...</strong></p>
</div> </div>
<div class="content-card settings-card"> <div class="content-card settings-card">
<h3 class="section-title"><i data-lucide="user-cog"></i> Profil</h3> <h3 class="section-title"><i data-lucide="user-cog"></i> Profil</h3>
{% if player.pending_display_name %} {% if player.pending_display_name %}
<div class="notice-card warning-card" style="padding: var(--spacing-medium);"> <div class="notice-card warning-card" style="padding: var(--spacing-medium); border-radius: var(--border-radius-small);">
<p class="notice-text" style="margin: 0;">Der Name <strong>{{ player.pending_display_name }}</strong> wird geprüft&hellip;</p> <p class="notice-text" style="margin-top: 0;"><strong>{{ player.pending_display_name }}</strong> wird geprüft…</p>
<form action="{{ url_for('handle_player_action') }}" method="POST" class="asynchronous-form">
<input type="hidden" name="action_type" value="cancel_name_change">
<button type="submit" class="action-button secondary-button small-button"><i data-lucide="x"></i> Abbrechen</button>
</form>
</div> </div>
{% else %} {% else %}
<button type="button" id="trigger-name-change-modal" class="action-button secondary-button"><i data-lucide="edit-3"></i> Name ändern</button> <button type="button" id="trigger-name-change-modal" class="action-button secondary-button"><i data-lucide="edit-3"></i> Name ändern</button>
@@ -122,45 +136,38 @@
<div id="elimination-confirm-modal" class="modal-overlay"> <div id="elimination-confirm-modal" class="modal-overlay">
<div class="modal-dialog"> <div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="alert-triangle"></i> Kill</h2> <h2 class="modal-title"><i data-lucide="alert-triangle"></i> Kill melden</h2>
<p class="modal-description">Möchtest du melden, dass du dein Ziel gekillt hast?</p> <p class="modal-description">Sicher, dass du den Kill melden möchtest?</p>
<div class="modal-actions"> <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="proceed-elimination" class="action-button danger-button"><i data-lucide="crosshair"></i> Melden</button>
<button type="button" id="cancel-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button> <button type="button" id="cancel-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button>
</div> </div>
</div> </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 id="rejection-modal" class="modal-overlay">
<div class="modal-dialog"> <div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="x-octagon"></i> Abgelehnt</h2> <h2 class="modal-title"><i data-lucide="x-octagon"></i> Abgelehnt</h2>
<p class="modal-description">Das Ziel hat deinen Kill abgelehnt.</p> <p class="modal-description">Das Ziel hat die Meldung abgelehnt. Bitte klären.</p>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" id="acknowledge-rejection" class="action-button primary-button"><i data-lucide="check"></i> Verstanden</button> <button type="button" id="acknowledge-rejection" class="action-button primary-button"><i data-lucide="check"></i> Okay</button>
</div> </div>
</div> </div>
</div> </div>
<div id="verification-modal" class="modal-overlay {% if player.pending_attacker_identifier %}active-modal{% endif %}"> <div id="verification-modal" class="modal-overlay {% if player.pending_attacker_identifier %}active-modal{% endif %}">
<div class="modal-dialog"> <div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="shield-alert"></i> Wurdest du eliminiert?</h2> <h2 class="modal-title"><i data-lucide="shield-alert"></i> Gekillt?</h2>
<p class="modal-description"> <p class="modal-description">
{% if pending_attacker_name %} {% if pending_attacker_name %}
<strong>{{ pending_attacker_name }}</strong> hat gemeldet, dass er/sie dich gekillt hat. Wurdest du von <strong>{{ pending_attacker_name }}</strong> gekillt?
{% else %} {% else %}
Dein Angreifer hat gemeldet, dass er dich gekillt hat. Wurdest du gekillt?
{% endif %} {% endif %}
</p> </p>
<div class="modal-actions"> <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="confirm-elimination" class="action-button danger-button"><i data-lucide="check"></i> Ja</button>
<button type="button" id="reject-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Ablehnen</button> <button type="button" id="reject-elimination" class="action-button secondary-button"><i data-lucide="x"></i> Nein</button>
</div> </div>
</div> </div>
</div> </div>
@@ -168,7 +175,7 @@
<div id="name-change-modal" class="modal-overlay"> <div id="name-change-modal" class="modal-overlay">
<div class="modal-dialog"> <div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="edit-3"></i> Name ändern</h2> <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> <p class="modal-description">Der Name wird vorher von der Spielleitung geprüft.</p>
<form action="{{ url_for('handle_player_action') }}" method="POST" class="input-form asynchronous-form"> <form action="{{ url_for('handle_player_action') }}" method="POST" class="input-form asynchronous-form">
<input type="hidden" name="action_type" value="request_name_change"> <input type="hidden" name="action_type" value="request_name_change">
<div class="form-group" style="text-align: left;"> <div class="form-group" style="text-align: left;">
@@ -176,8 +183,8 @@
<input type="text" id="pending_display_name" name="pending_display_name" class="text-input" required> <input type="text" id="pending_display_name" name="pending_display_name" class="text-input" required>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button type="submit" class="action-button primary-button"><i data-lucide="send"></i> Abschicken</button> <button type="submit" class="action-button primary-button"><i data-lucide="send"></i> Speichern</button>
<button type="button" id="cancel-name-change" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button> <button type="button" id="cancel-name-change-modal" class="action-button secondary-button"><i data-lucide="x"></i> Abbrechen</button>
</div> </div>
</form> </form>
</div> </div>
+2
View File
@@ -14,10 +14,12 @@
</head> </head>
<body> <body>
<header class="application-header"> <header class="application-header">
<a href="/" class="brand-link">
<div class="brand-container"> <div class="brand-container">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Logo" class="brand-logo"> <img src="{{ url_for('static', filename='logo.svg') }}" alt="Logo" class="brand-logo">
<h1 class="application-title"><i>SCO</i>icide Squad</h1> <h1 class="application-title"><i>SCO</i>icide Squad</h1>
</div> </div>
</a>
<nav class="application-navigation"> <nav class="application-navigation">
{% if session.get('player_identifier') or session.get('administrator_authenticated') %} {% if session.get('player_identifier') or session.get('administrator_authenticated') %}
<a href="{{ url_for('handle_logout') }}" class="action-button secondary-button icon-button"> <a href="{{ url_for('handle_logout') }}" class="action-button secondary-button icon-button">
+29 -16
View File
@@ -11,18 +11,18 @@
<input type="password" id="password_string" name="password_string" class="text-input" required> <input type="password" id="password_string" name="password_string" class="text-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label> <label for="password_string_confirmation" class="input-label">Passwort bestätigen</label>
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required> <input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
</div> </div>
<button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Passwort speichern</button> <button type="submit" class="action-button primary-button"><i data-lucide="save"></i> Speichern</button>
</form> </form>
</div> </div>
{% elif register_mode %} {% elif register_mode %}
<div class="content-card"> <div class="content-card">
<h2 class="section-title"><i data-lucide="user-plus"></i> Neues Konto erstellen</h2> <h2 class="section-title"><i data-lucide="user-plus"></i> Registrieren</h2>
<form action="{{ url_for('handle_registration') }}" method="POST" class="input-form"> <form action="{{ url_for('handle_registration') }}" method="POST" class="input-form">
<div class="form-group"> <div class="form-group">
<label for="display_name" class="input-label">Dein Name</label> <label for="display_name" class="input-label">Name</label>
<input type="text" id="display_name" name="display_name" class="text-input" required> <input type="text" id="display_name" name="display_name" class="text-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -30,32 +30,34 @@
<input type="password" id="password_string" name="password_string" class="text-input" required> <input type="password" id="password_string" name="password_string" class="text-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password_string_confirmation" class="input-label">Passwort wiederholen</label> <label for="password_string_confirmation" class="input-label">Passwort bestätigen</label>
<input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required> <input type="password" id="password_string_confirmation" name="password_string_confirmation" class="text-input" required>
</div> </div>
<button type="submit" class="action-button primary-button"><i data-lucide="check-circle"></i> Registrieren</button> <button type="submit" class="action-button primary-button"><i data-lucide="check-circle"></i> Konto erstellen</button>
</form> </form>
</div> </div>
<div class="content-card" style="text-align: center;"> <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> <a href="{{ url_for('handle_login') }}" class="action-button secondary-button"><i data-lucide="arrow-left"></i> Zur Anmeldung</a>
</div> </div>
{% elif registration_success %} {% elif registration_success %}
<div class="content-card notice-card success-card" style="text-align: center;"> <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> <h2 class="section-title" style="justify-content: center;"><i data-lucide="party-popper"></i> Erfolgreich</h2>
<p>Deine ID für die Anmeldung lautet:</p> <h2 style="color: var(--fallback-danger); color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger));">Wichtig!</h2>
<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>Deine ID:</p>
<p>Bitte speichere oder merke sie dir!</p> <h1 class="monospace-identifier" style="font-size: calc(3rem * var(--dimension-multiplier)); letter-spacing: calc(0.25rem * var(--dimension-multiplier)); color: var(--fallback-primary); color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); margin: var(--spacing-medium) 0;">{{ new_identifier }}</h1>
<button type="button" id="copy-identifier-button" class="action-button secondary-button" data-identifier="{{ new_identifier }}"><i data-lucide="copy"></i> ID kopieren</button>
<p style="margin-top: var(--spacing-medium);">Gut aufbewahren!</p>
<div style="margin-top: var(--spacing-large);"> <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> <a href="{{ url_for('handle_login', prefill_identifier=new_identifier) }}" class="action-button primary-button"><i data-lucide="log-in"></i> Anmelden</a>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="content-card"> <div class="content-card">
<h2 class="section-title"><i data-lucide="lock"></i> Anmeldung</h2> <h2 class="section-title"><i data-lucide="lock"></i> Login</h2>
<form action="{{ url_for('handle_login') }}" method="POST" class="input-form"> <form action="{{ url_for('handle_login') }}" method="POST" class="input-form">
<div class="form-group"> <div class="form-group">
<label for="player_identifier" class="input-label">ID</label> <label for="player_identifier" class="input-label">ID</label>
<input type="text" id="player_identifier" name="player_identifier" class="text-input" style="text-transform: uppercase;" maxlength="{{ identifier_length }}" oninput="this.value = this.value.replace(/[^a-zA-Z]/g, '').toUpperCase().slice(0, {{ identifier_length }});" pattern="[A-Za-z]{{ '{' }}{{ identifier_length }}{{ '}' }}" required> <input type="text" id="player_identifier" name="player_identifier" class="text-input" value="{{ request.args.get('prefill_identifier', '') }}" style="text-transform: uppercase;" maxlength="{{ identifier_length }}" oninput="this.value = this.value.replace(/[^a-zA-Z]/g, '').toUpperCase().slice(0, {{ identifier_length }});" pattern="[A-Za-z]{{ '{' }}{{ identifier_length }}{{ '}' }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password_string" class="input-label">Passwort</label> <label for="password_string" class="input-label">Passwort</label>
@@ -64,8 +66,19 @@
<button type="submit" class="action-button primary-button"><i data-lucide="log-in"></i> Anmelden</button> <button type="submit" class="action-button primary-button"><i data-lucide="log-in"></i> Anmelden</button>
</form> </form>
</div> </div>
<div class="content-card" style="text-align: center;"> <div class="content-card" style="text-align: center; display: flex; flex-direction: column; gap: var(--spacing-medium);">
<a href="{{ url_for('handle_registration') }}" class="action-button secondary-button"><i data-lucide="user-plus"></i> Noch kein Konto? Hier registrieren</a> <a href="{{ url_for('handle_registration') }}" class="action-button secondary-button"><i data-lucide="user-plus"></i> Registrieren</a>
<button type="button" id="trigger-help-modal" class="action-button secondary-button"><i data-lucide="life-buoy"></i> Probleme beim Login?</button>
</div>
<div id="help-modal" class="modal-overlay">
<div class="modal-dialog">
<h2 class="modal-title"><i data-lucide="help-circle"></i> Hilfe</h2>
<p class="modal-description">Wende dich an die Spielleitung, falls du deine ID oder dein Passwort vergessen hast.</p>
<div class="modal-actions">
<button type="button" id="close-help-modal" class="action-button primary-button"><i data-lucide="check"></i> Okay</button>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
</section> </section>