commit d8c301215bf9274b252a317f230f6528218a0d0c Author: Jamon Date: Fri Aug 9 17:00:28 2024 +1200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0ea6a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# .gitignore +config.py +*.pyc +__pycache__/ +.env +venv/ +src-tauri/ +node-modules/ +game-assets/ +backup/ +update_users.py +skinimporter.py +package.json +package-lock.json +favicon_io.zip +classcreator.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b25ba9 Binary files /dev/null and b/README.md differ diff --git a/bottoken b/bottoken new file mode 100644 index 0000000..085b61d --- /dev/null +++ b/bottoken @@ -0,0 +1,5 @@ +MTI2OTc4NjE5MjAwNjg3MzE2Mg.GavQq6.meQHlCJFU8eI8ICld_pJjVXghB0XvjH2vxkIZE + + +secret +633l3SxReY_OWAhERjt-nY3_GP57p6OA \ No newline at end of file diff --git a/discordbot.py b/discordbot.py new file mode 100644 index 0000000..4ea3ee3 --- /dev/null +++ b/discordbot.py @@ -0,0 +1,191 @@ +import discord +from discord import app_commands +from discord.ext import commands +import pymongo +from bson.objectid import ObjectId +import logging +from config import DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI, DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, ACCOUNT_LINKED_ROLE_ID, DISCORD_WEBHOOK_URL + +# Set up logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s') +logger = logging.getLogger('resonance_rumble_bot') + + +# MongoDB setup +client = pymongo.MongoClient('mongodb://localhost:27017/') +db = client['resonance_rumble'] +classes_collection = db['classes'] +weapons_collection = db['weapons'] +skins_collection = db['skins'] +users_collection = db['users'] + + +intents = discord.Intents.all() +intents.message_content = True #v2 +intents.members = True +bot = commands.Bot(command_prefix='!', intents=intents) + +LINKED_ROLE_NAME = "Account-linked" + + +@bot.event +async def on_ready(): + print(f'{bot.user} has connected to Discord!') + try: + synced = await bot.tree.sync() + print(f"Synced {len(synced)} command(s)") + except Exception as e: + print(f"Error syncing commands: {e}") + +@bot.event +async def on_member_join(member): + logger.info(f"New member joined: {member.name} (ID: {member.id})") + role = discord.utils.get(member.guild.roles, name="Member") + if role: + try: + await member.add_roles(role) + logger.info(f"Successfully added Member role to {member.name}") + except discord.Forbidden: + logger.error(f"Failed to add Member role to {member.name}: Bot doesn't have permission") + except discord.HTTPException as e: + logger.error(f"Failed to add Member role to {member.name}: {str(e)}") + else: + logger.error(f"'Member' role not found in the server") + +async def class_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: + classes = classes_collection.find({}, {"name": 1}) + return [ + app_commands.Choice(name=class_doc["name"], value=class_doc["name"]) + for class_doc in classes if current.lower() in class_doc["name"].lower() + ][:25] # Discord limits to 25 choices + +@bot.tree.command(name="class_info", description="Get information about a game class") +@app_commands.autocomplete(class_name=class_autocomplete) +async def class_info(interaction: discord.Interaction, class_name: str): + class_data = classes_collection.find_one({"name": class_name}) + if class_data: + embed = discord.Embed(title=f"{class_name} Class Info", color=0x00ff00) + embed.add_field(name="Health", value=class_data['base_attributes']['health'], inline=True) + embed.add_field(name="Speed", value=class_data['base_attributes']['speed'], inline=True) + embed.add_field(name="Damage Multiplier", value=class_data['base_attributes']['damage_multiplier'], inline=True) + embed.add_field(name="Weapon Limit", value=class_data['weapon_limit'], inline=True) + await interaction.response.send_message(embed=embed) + else: + await interaction.response.send_message(f"Class '{class_name}' not found.", ephemeral=True) + +async def weapon_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: + weapons = weapons_collection.find({}, {"name": 1}) + return [ + app_commands.Choice(name=weapon["name"], value=weapon["name"]) + for weapon in weapons if current.lower() in weapon["name"].lower() + ][:25] + +@bot.tree.command(name="weapon_info", description="Get information about a weapon") +@app_commands.autocomplete(weapon_name=weapon_autocomplete) +async def weapon_info(interaction: discord.Interaction, weapon_name: str): + weapon_data = weapons_collection.find_one({"name": weapon_name}) + if weapon_data: + embed = discord.Embed(title=f"{weapon_name} Weapon Info", color=0x0000ff) + embed.add_field(name="Type", value=weapon_data['weapon_type'], inline=True) + embed.add_field(name="Damage", value=weapon_data['base_attributes']['damage'], inline=True) + embed.add_field(name="Fire Rate", value=weapon_data['base_attributes']['fire_rate'], inline=True) + embed.add_field(name="Bullet Speed", value=weapon_data['base_attributes']['bullet_speed'], inline=True) + await interaction.response.send_message(embed=embed) + else: + await interaction.response.send_message(f"Weapon '{weapon_name}' not found.", ephemeral=True) + +async def skin_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: + skins = skins_collection.find({}, {"name": 1}) + return [ + app_commands.Choice(name=skin["name"], value=skin["name"]) + for skin in skins if current.lower() in skin["name"].lower() + ][:25] + +@bot.tree.command(name="skin_info", description="Get information about a skin") +@app_commands.autocomplete(skin_name=skin_autocomplete) +async def skin_info(interaction: discord.Interaction, skin_name: str): + skin_data = skins_collection.find_one({"name": skin_name}) + if skin_data: + embed = discord.Embed(title=f"{skin_name} Skin Info", color=0xff00ff) + embed.add_field(name="Type", value=skin_data['type'], inline=True) + embed.add_field(name="Rarity", value=skin_data['rarity'], inline=True) + embed.add_field(name="Effect", value=skin_data['effect'], inline=True) + await interaction.response.send_message(embed=embed) + else: + await interaction.response.send_message(f"Skin '{skin_name}' not found.", ephemeral=True) + +@bot.tree.command(name="leaderboard", description="Show the top players") +@app_commands.describe(category="The category for the leaderboard") +@app_commands.choices(category=[ + app_commands.Choice(name="Level", value="level"), + app_commands.Choice(name="Synth Coins", value="synth_coins"), + app_commands.Choice(name="Experience", value="experience") +]) +async def leaderboard(interaction: discord.Interaction, category: app_commands.Choice[str]): + top_players = list(db.users.find().sort(category.value, -1).limit(10)) + + embed = discord.Embed(title=f"Top 10 Players - {category.name}", color=0xffa500) + for i, player in enumerate(top_players, 1): + embed.add_field(name=f"{i}. {player['username']}", value=f"{category.name}: {player.get(category.value, 0)}", inline=False) + + await interaction.response.send_message(embed=embed) + + +@bot.tree.command(name="check_permissions", description="Check bot's permissions") +@commands.has_permissions(administrator=True) +async def check_permissions(interaction: discord.Interaction): + bot_member = interaction.guild.get_member(bot.user.id) + permissions = bot_member.guild_permissions + + embed = discord.Embed(title="Bot Permissions", color=0x00ff00) + crucial_perms = ["manage_roles", "view_channel", "send_messages", "embed_links"] + + for perm, value in permissions: + if perm in crucial_perms: + embed.add_field(name=perm.replace('_', ' ').title(), value=str(value), inline=False) + + await interaction.response.send_message(embed=embed) + +@bot.tree.command(name="link_status", description="Check your Resonance Rumble account link status") +async def link_status(interaction: discord.Interaction): + user = users_collection.find_one({'discord_id': str(interaction.user.id)}) + if user: + await add_linked_role(interaction.user) + await interaction.response.send_message(f"Your Discord account is linked to the Resonance Rumble account: {user['username']}") + else: + await interaction.response.send_message("Your Discord account is not linked to any Resonance Rumble account. Use the in-game menu to link your account.") + +async def add_linked_role(member): + guild = member.guild + role = discord.utils.get(guild.roles, name=LINKED_ROLE_NAME) + if role is None: + # Create the role if it doesn't exist + role = await guild.create_role(name=LINKED_ROLE_NAME, color=discord.Color.blue()) + + if role not in member.roles: + try: + await member.add_roles(role) + print(f"Added {LINKED_ROLE_NAME} role to {member.name}") + except discord.Forbidden: + print(f"Bot doesn't have permission to add roles to {member.name}") + except discord.HTTPException as e: + print(f"Failed to add role to {member.name}: {str(e)}") + +# You might want to add a command to manually sync roles +@bot.tree.command(name="sync_linked_role", description="Sync the Account-linked role for all linked users") +@commands.has_permissions(administrator=True) +async def sync_linked_role(interaction: discord.Interaction): + await interaction.response.defer() + guild = interaction.guild + linked_users = users_collection.find({'discord_id': {'$exists': True}}) + count = 0 + for user in linked_users: + member = guild.get_member(int(user['discord_id'])) + if member: + await add_linked_role(member) + count += 1 + await interaction.followup.send(f"Synced roles for {count} linked users.") + + + +bot.run(DISCORD_BOT_TOKEN) \ No newline at end of file diff --git a/game_server.py b/game_server.py new file mode 100644 index 0000000..35eb04d --- /dev/null +++ b/game_server.py @@ -0,0 +1,1810 @@ +# game_server.py +from flask import Flask, send_from_directory, jsonify, request, redirect, session, make_response +from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect +from pymongo import MongoClient +from bson.objectid import ObjectId +import random +import time +import math +from pydub import AudioSegment +from pydub.utils import make_chunks +import threading +import cmd +import numpy as np +import io +import os +import ssl +from werkzeug.utils import secure_filename +from threading import Thread +import bcrypt +import jwt +from functools import wraps +import datetime +import re +from bson.json_util import dumps +from bson import json_util +from bson import ObjectId +import json +import logging +import requests +from urllib.parse import urlencode +import secrets +from werkzeug.http import generate_etag +from config import DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI, DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, ACCOUNT_LINKED_ROLE_ID, DISCORD_WEBHOOK_URL + +secret_key = secrets.token_hex(16) + +DISCORD_API_ENDPOINT = 'https://discord.com/api/v10' +DISCORD_OAUTH_STATE_EXPIRY = 600 # 10 minutes + +app = Flask(__name__) +app.config['SECRET_KEY'] = secret_key +socketio = SocketIO(app, cors_allowed_origins="*") + +# MongoDB setup +client = MongoClient('mongodb://localhost:27017/') +db = client['resonance_rumble'] +users_collection = db['users'] +sessions_collection = db['sessions'] +skins_collection = db['skins'] +weapons_collection = db['weapons'] +classes_collection = db['classes'] +enemies_collection = db['enemies'] +active_sockets_collection = db['active_sockets'] +oauth_states = db['oauth_states'] + +def decode_token(token): + try: + payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) + return payload['user_id'] + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + +def token_required(f): + @wraps(f) + def decorated(*args, **kwargs): + token = request.headers.get('Authorization') + if not token: + return jsonify({'error': 'Token is missing'}), 401 + user_id = decode_token(token) + if not user_id: + return jsonify({'error': 'Token is invalid or expired'}), 401 + return f(user_id, *args, **kwargs) + return decorated + + +# Game state +main_room = 'main_game_room' + +MAP_WIDTH = 3000 +MAP_HEIGHT = 2000 + + + +def send_discord_alert(message): + data = { + "content": message + } + response = requests.post(DISCORD_WEBHOOK_URL, json=data) + if response.status_code != 204: + logging.error(f"Failed to send Discord alert: {response.status_code}, {response.text}") + +def send_alert(message): + socketio.emit('game_alert', {'message': message}, room=main_room) + +class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, ObjectId): + return str(obj) + elif isinstance(obj, Player): + return obj.to_dict() + elif isinstance(obj, SynthCoin): + return {'x': obj.x, 'y': obj.y, 'radius': obj.radius} + return super().default(obj) + + +app.json_encoder = CustomJSONEncoder + +def serialize_weapon(weapon): + serialized = {k: str(v) if isinstance(v, ObjectId) else v for k, v in weapon.items()} + return serialized + + +def serialize_game_state(game_state): + serialized_state = { + 'players': { + player_id: player.to_dict() for player_id, player in game_state['players'].items() + }, + 'enemies': game_state['enemies'], + 'bullets': game_state['bullets'], + 'synth_coins': [{'x': coin.x, 'y': coin.y, 'radius': coin.radius} for coin in game_state['synth_coins']], + # Add other necessary game state data here + } + return serialized_state + + +# New function to load music from the 'music' folder +def load_music_playlist(): + music_folder = 'music' + playlist = [] + for file in os.listdir(music_folder): + if file.endswith('.mp3'): + playlist.append(os.path.join(music_folder, file)) + return playlist + + +class SynthCoin: + def __init__(self, x, y): + self.x = x + self.y = y + self.radius = 5 + self.follow_radius = 100 + self.follow_speed = 2 + self.created_at = time.time() + + def update(self, players): + closest_player = None + closest_distance = float('inf') + + for player in players.values(): + if player.is_paused: + continue + + distance = math.hypot(player.x - self.x, player.y - self.y) + if distance < self.follow_radius and distance < closest_distance: + closest_player = player + closest_distance = distance + + if closest_player: + dx = closest_player.x - self.x + dy = closest_player.y - self.y + distance = math.hypot(dx, dy) + if distance > 0: + self.x += (dx / distance) * self.follow_speed + self.y += (dy / distance) * self.follow_speed + + +game_state = { + 'players': {}, + 'enemies': [], + 'bullets': [], + 'start_time': time.time(), + 'music_start_time': None, + 'current_song': None, + 'playlist': load_music_playlist(), # Load the playlist dynamically + 'song_position': 0, + 'is_playing': False, + 'synth_coins': [], + 'player_upgrade_options': {} +} + + + +MAX_ENEMIES = 10 +ENEMY_SPEED = 1 +BULLET_SPEED = 5 +BULLET_LIFETIME = 3 # seconds + +CHUNK_SIZE = 1024 # Size of each audio chunk to send + +active_players = set() + + +def generate_enemies(count): + enemy_templates = list(enemies_collection.find()) + if not enemy_templates: + print("No enemy templates found in the database") + return [] + + enemies = [] + for _ in range(count): + template = random.choice(enemy_templates) + enemy = { + 'id': str(ObjectId()), + 'name': template['name'], + 'x': random.randint(0, 1000), + 'y': random.randint(0, 1000), + 'image': template['image'], + 'attributes': template['attributes'].copy(), + 'difficulty': template['difficulty'], + 'current_health': template['attributes']['health'], + 'experience': int(template['attributes'].get('experience', 10)) # Default to 10 if not set + } + enemies.append(enemy) + return enemies + +def is_valid_input(input_string): + if not isinstance(input_string, str): + return False + # Check for MongoDB operators and other potentially harmful patterns + invalid_patterns = ['\$', '__proto__', 'constructor'] + return not any(pattern in input_string for pattern in invalid_patterns) + +@app.before_request +def before_request(): + if not request.is_secure: + url = request.url.replace('http://', 'https://', 1) + return redirect(url, code=301) + + +@app.route('/') +def index(): + return send_from_directory('.', 'index.html') + +@app.route('/initiate-discord-link', methods=['POST']) +@token_required +def initiate_discord_link(user_id): + state = secrets.token_urlsafe(32) + oauth_states.insert_one({ + 'state': state, + 'user_id': user_id, + 'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=DISCORD_OAUTH_STATE_EXPIRY) + }) + + params = { + 'client_id': DISCORD_CLIENT_ID, + 'redirect_uri': DISCORD_REDIRECT_URI, + 'response_type': 'code', + 'scope': 'identify', + 'state': state + } + auth_url = f'{DISCORD_API_ENDPOINT}/oauth2/authorize?{urlencode(params)}' + return jsonify({'auth_url': auth_url}) + +@app.route('/discord-callback') +def discord_callback(): + code = request.args.get('code') + state = request.args.get('state') + + if not code or not state: + return "Error: Missing parameters", 400 + + # Verify state and get user_id + oauth_state = oauth_states.find_one_and_delete({ + 'state': state, + 'expires_at': {'$gt': datetime.datetime.utcnow()} + }) + + if not oauth_state: + return "Error: Invalid or expired state", 400 + + user_id = oauth_state['user_id'] + + # Exchange code for token + data = { + 'client_id': DISCORD_CLIENT_ID, + 'client_secret': DISCORD_CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': DISCORD_REDIRECT_URI + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = requests.post(f'{DISCORD_API_ENDPOINT}/oauth2/token', data=data, headers=headers) + + if r.status_code != 200: + return "Error: Failed to exchange code for token", 400 + + tokens = r.json() + + # Get user info + headers = { + 'Authorization': f'Bearer {tokens["access_token"]}' + } + r = requests.get(f'{DISCORD_API_ENDPOINT}/users/@me', headers=headers) + + if r.status_code != 200: + return "Error: Failed to get user info", 400 + + user_data = r.json() + + # Update user in database + users_collection.update_one( + {'_id': ObjectId(user_id)}, + {'$set': { + 'discord_id': user_data['id'], + 'discord_username': f"{user_data['username']}#{user_data['discriminator']}", + 'discord_avatar': user_data['avatar'] + }} + ) + + # After successfully updating the user in the database: + add_discord_role(user_data['id'], ACCOUNT_LINKED_ROLE_ID) + + + return """ + + """ + +@app.route('/unlink-discord', methods=['POST']) +@token_required +def unlink_discord(user_id): + user = users_collection.find_one({'_id': ObjectId(user_id)}) + if not user: + return jsonify({'success': False, 'message': 'User not found'}), 404 + + discord_id = user.get('discord_id') + if discord_id: + remove_discord_role(discord_id, ACCOUNT_LINKED_ROLE_ID) + + result = users_collection.update_one( + {'_id': ObjectId(user_id)}, + {'$unset': { + 'discord_id': '', + 'discord_username': '', + 'discord_avatar': '' + }} + ) + + if result.modified_count > 0: + return jsonify({'success': True, 'message': 'Discord account unlinked successfully'}) + else: + return jsonify({'success': False, 'message': 'Failed to unlink Discord account'}), 500 + + +@app.route('/get-discord-info') +@token_required +def get_discord_info(user_id): + user = users_collection.find_one({'_id': ObjectId(user_id)}) + if not user: + return jsonify({'error': 'User not found'}), 404 + + return jsonify({ + 'linked': 'discord_id' in user, + 'username': user.get('discord_username'), + 'avatar': user.get('discord_avatar'), + 'discord_id': user.get('discord_id') + }) + + + +@app.route('/.well-known/acme-challenge/') +def acme_challenge(filename): + return send_from_directory(os.path.join(app.root_path, '.well-known', 'acme-challenge'), + filename) + +@app.route('/get-enemy-images') +def get_enemy_images(): + enemy_images = list(enemies_collection.distinct('image')) + return jsonify(enemy_images) + + +@app.route('/') +def serve_static(filename): + # Ensure the filename is secure + filename = secure_filename(filename) + + # Check if the file has an allowed extension + allowed_extensions = ['.mp3', '.js', '.png', '.ico', '.webmanifest', '.css', '.gif'] + if not any(filename.lower().endswith(ext) for ext in allowed_extensions): + return "Access denied", 403 + + # Determine the appropriate directory based on file extension and path + if filename.lower().endswith('.mp3'): + file_directory = 'music' + elif filename.lower().endswith('.js'): + file_directory = 'static/js' + elif filename.lower().startswith('hats/'): + file_directory = 'static' # Hats are in static/hats + elif filename.lower().startswith('images/'): + file_directory = 'static' # Images are in static/images + else: + file_directory = 'static' # For favicon files and webmanifest + + # Construct the full path + file_path = os.path.join(file_directory, filename) + + # Check if the file exists and is within the intended directory + if not os.path.exists(file_path) or not os.path.abspath(file_path).startswith(os.path.abspath(file_directory)): + return "File not found", 404 + + return send_from_directory(file_directory, filename) + +@app.route('/get-shop-items', methods=['POST']) +def get_shop_items(): + data = request.json + player_class_id = data.get('player_class') + + if not player_class_id: + return jsonify({'error': 'Player class not provided'}), 400 + + player_class = classes_collection.find_one({'_id': ObjectId(player_class_id)}) + if not player_class: + return jsonify({'error': 'Invalid player class'}), 400 + + allowed_types = player_class['weapon_restrictions']['allowed_types'] + disallowed_types = player_class['weapon_restrictions']['disallowed_types'] + + weapons = list(weapons_collection.find({ + 'weapon_type': {'$in': allowed_types, '$nin': disallowed_types} + })) + + for weapon in weapons: + weapon['_id'] = str(weapon['_id']) + + return jsonify(weapons), 200 + +# Add this new route to handle weapon purchase +@app.route('/buy-weapon', methods=['POST']) +def buy_weapon(): + data = request.json + player_id = data.get('player_id') + weapon_id = data.get('weapon_id') + + if not player_id or not weapon_id: + return jsonify({'success': False, 'message': 'Invalid request'}), 400 + + player = game_state['players'].get(player_id) + if not player: + return jsonify({'success': False, 'message': 'Player not found'}), 404 + + weapon = weapons_collection.find_one({'_id': ObjectId(weapon_id)}) + if not weapon: + return jsonify({'success': False, 'message': 'Weapon not found'}), 404 + + weapon_price = int(weapon.get('price', 0)) + + if player.synth_coins < weapon_price: + return jsonify({'success': False, 'message': 'Not enough Synth Coins'}), 400 + + if len(player.weapons) >= player.weapon_limit: + return jsonify({'success': False, 'message': 'Weapon limit reached'}), 400 + + player.synth_coins -= weapon_price + player.add_weapon(weapon) + + # Notify all clients about the updated player data + socketio.emit('player_update', { + 'id': player_id, + 'player': player.to_dict() + }, room=main_room) + + return jsonify({ + 'success': True, + 'message': 'Weapon purchased successfully', + 'updatedPlayerData': { + 'synth_coins': player.synth_coins, + 'weapons': [serialize_weapon(w) for w in player.weapons] + } + }), 200 + + + +@app.route('/get-available-classes', methods=['GET']) +def get_available_classes(): + classes = list(classes_collection.find()) + for class_data in classes: + class_data['_id'] = str(class_data['_id']) + class_data['starting_weapon'] = weapons_collection.find_one({'_id': class_data['starting_weapon']}) + class_data['starting_weapon']['_id'] = str(class_data['starting_weapon']['_id']) + return jsonify(classes) + +@app.route('/select-class', methods=['POST']) +def select_class(): + data = request.json + username = data.get('username') + class_id = data.get('class_id') + + if not class_id: + return jsonify({'error': 'Missing class_id'}), 400 + + player_class = classes_collection.find_one({'_id': ObjectId(class_id)}) + if not player_class: + return jsonify({'error': 'Class not found'}), 404 + + if not player_class['unlocked_by_default']: + if not username: + return jsonify({'error': 'Username required for locked classes'}), 403 + + user = users_collection.find_one({'username': username}) + if not user: + return jsonify({'error': 'User not found'}), 404 + + # Check if the user has met the unlock requirements + # This is a placeholder and should be implemented based on your game's logic + return jsonify({'error': 'Class is locked'}), 403 + + if username: + users_collection.update_one( + {'username': username}, + {'$set': {'selected_class': ObjectId(class_id)}} + ) + + return jsonify({'success': True, 'message': 'Class selected successfully'}), 200 + + +@app.route('/get-available-skins', methods=['GET']) +def get_available_skins(): + # Retrieve all skins from the database + all_skins = list(skins_collection.find()) + + # Convert ObjectId to string for JSON serialization + for skin in all_skins: + skin['_id'] = str(skin['_id']) + + return jsonify(all_skins), 200 + +@app.route('/get-unlocked-skins', methods=['POST']) +def get_unlocked_skins(): + data = request.json + username = data.get('username') + + if not username: + return jsonify({'error': 'Username is required'}), 400 + + user = users_collection.find_one({'username': username}) + if not user: + return jsonify({'error': 'User not found'}), 404 + + unlocked_skins = user.get('unlocked_skins', []) + last_modified = user.get('skins_last_modified', datetime.datetime.utcnow()) + + # Fetch the details of each unlocked skin + unlocked_skin_details = [] + for skin_id in unlocked_skins: + skin = skins_collection.find_one({'_id': skin_id}) + if skin: + unlocked_skin_details.append({ + 'id': str(skin['_id']), + 'type': skin['type'], + 'name': skin['name'], + 'value': skin['value'], + 'effect': skin['effect'], + 'rarity': skin['rarity'] + }) + + response_data = { + 'skins': unlocked_skin_details, + 'last_modified': last_modified.isoformat() + } + + # Use json_util to handle MongoDB-specific types + response_json = json.dumps(response_data, default=json_util.default) + etag = generate_etag(response_json.encode('utf-8')) + + if request.if_none_match and etag in request.if_none_match: + return '', 304 + + response = jsonify(response_data) + response.set_etag(etag) + response.last_modified = last_modified + return response, 200 + + +@app.route('/select-skin', methods=['POST']) +def select_skin(): + try: + data = request.json + username = data.get('username') + skin_type = data.get('skin_type') + skin_id = data.get('skin_id') + + if not all([username, skin_type, skin_id]): + return jsonify({'error': 'Missing required fields'}), 400 + + user = users_collection.find_one({'username': username}) + if not user: + return jsonify({'error': 'User not found'}), 404 + + try: + skin_object_id = ObjectId(skin_id) + except: + return jsonify({'error': 'Invalid skin ID format'}), 400 + + # Verify that the skin exists and is unlocked for the user + skin = skins_collection.find_one({'_id': skin_object_id}) + if not skin or skin_object_id not in user.get('unlocked_skins', []): + return jsonify({'error': 'Invalid or locked skin'}), 400 + + # Update the user's selected skins and last modified time + result = users_collection.update_one( + {'username': username}, + { + '$set': { + f'selected_skins.{skin_type}': skin_object_id, + 'skins_last_modified': datetime.datetime.utcnow() + } + } + ) + + if result.modified_count == 0: + # Check if the skin was already selected + current_skin = user.get('selected_skins', {}).get(skin_type) + if current_skin and current_skin == skin_object_id: + return jsonify({'success': True, 'message': 'Skin already selected'}), 200 + else: + return jsonify({'error': 'Failed to update skin'}), 500 + + # Fetch the updated skin data + updated_skin_data = { + skin_type: { + 'name': skin['name'], + 'value': skin['value'], + 'effect': skin['effect'] + } + } + + # Find the player's socket ID + player_socket_id = None + for sid, player in game_state['players'].items(): + if player.name == username: + player_socket_id = sid + break + + if player_socket_id: + # Update the player's skin in the game state + game_state['players'][player_socket_id].skin.update(updated_skin_data) + + # Emit a 'skin_update' event to all connected clients + socketio.emit('skin_update', { + 'player_id': player_socket_id, + 'skin_data': updated_skin_data + }, room=main_room) + + return jsonify({'success': True, 'message': 'Skin selected and updated successfully'}), 200 + + except Exception as e: + app.logger.error(f"Error in select_skin: {str(e)}") + return jsonify({'error': 'An internal error occurred'}), 500 + +@app.route('/signup', methods=['POST']) +def signup(): + try: + data = request.json + username = data.get('username') + email = data.get('email') + password = data.get('password') + + if not username or not email or not password: + return jsonify({'success': False, 'message': 'All fields are required'}), 400 + + # Username validation + if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username): + return jsonify({'success': False, 'message': 'Username must be 3-20 characters long and contain only letters, numbers, and underscores'}), 400 + + # Email validation + email_regex = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') + if not email_regex.match(email): + return jsonify({'success': False, 'message': 'Invalid email format'}), 400 + + # Password length validation + if len(password) < 8: + return jsonify({'success': False, 'message': 'Password must be at least 8 characters long'}), 400 + + # Check for potentially harmful input + if not is_valid_input(username) or not is_valid_input(email): + return jsonify({'success': False, 'message': 'Invalid input detected'}), 400 + + existing_user = users_collection.find_one({'$or': [{'username': username}, {'email': email}]}) + if existing_user: + return jsonify({'success': False, 'message': 'Username or email already exists'}), 400 + + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + + default_classes = list(classes_collection.find({'unlocked_by_default': True})) + default_class_ids = [str(class_doc['_id']) for class_doc in default_classes] + + new_user = { + 'username': username, + 'email': email, + 'password': hashed_password, + 'created_at': datetime.datetime.utcnow(), + "selected_skins": { + "color": ObjectId("669e1272813db368f3b19519"), # Glowy Blue + "hat": ObjectId(), + "image": ObjectId() + }, + "unlocked_skins": [ObjectId("669e1272813db368f3b19519")], # Glowy Blue + "unlocked_classes": default_class_ids, + "selected_class": default_class_ids[0] if default_class_ids else None + } + users_collection.insert_one(new_user) + + return jsonify({'success': True, 'message': 'User created successfully'}), 201 + except Exception as e: + app.logger.error(f"Error during signup: {str(e)}") + return jsonify({'success': False, 'message': 'An error occurred during signup'}), 500 + + +@app.route('/login', methods=['POST']) +def login(): + data = request.json + username = data.get('username') + password = data.get('password') + + user = users_collection.find_one({'username': username}) + if not user or not bcrypt.checkpw(password.encode('utf-8'), user['password']): + return jsonify({'success': False, 'message': 'Invalid username or password'}), 401 + + try: + session_token = jwt.encode( + {'user_id': str(user['_id']), 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)}, + app.config['SECRET_KEY'], + algorithm='HS256' + ) + + # If using PyJWT < 2.0.0, session_token will be bytes, so we need to decode it + if isinstance(session_token, bytes): + session_token = session_token.decode('utf-8') + + sessions_collection.insert_one({ + 'user_id': user['_id'], + 'token': session_token, + 'created_at': datetime.datetime.utcnow() + }) + + return jsonify({'success': True, 'sessionToken': session_token}), 200 + except Exception as e: + app.logger.error(f"Error during login: {str(e)}") + return jsonify({'success': False, 'message': 'An error occurred during login'}), 500 + +@app.route('/verify-session', methods=['POST']) +def verify_session(): + data = request.json + session_token = data.get('sessionToken') + + if not session_token: + return jsonify({'valid': False, 'message': 'No session token provided'}), 401 + + try: + payload = jwt.decode(session_token, app.config['SECRET_KEY'], algorithms=['HS256']) + user_id = payload['user_id'] + user = users_collection.find_one({'_id': ObjectId(user_id)}) + + if user: + return jsonify({'valid': True, 'username': user['username']}), 200 + else: + return jsonify({'valid': False, 'message': 'User not found'}), 401 + except jwt.ExpiredSignatureError: + return jsonify({'valid': False, 'message': 'Session expired'}), 401 + except jwt.InvalidTokenError: + return jsonify({'valid': False, 'message': 'Invalid token'}), 401 + except Exception as e: + app.logger.error(f"Error during session verification: {str(e)}") + return jsonify({'valid': False, 'message': 'An error occurred during session verification'}), 500 + +# Update the get_player_skin function +@app.route('/get-player-skin', methods=['POST']) +def get_player_skin(): + data = request.json + username = data.get('username') + + if not username: + return jsonify({'error': 'Username is required'}), 400 + + # Check if the username is a guest + if username.startswith('Guest-'): + # Return a default skin for guests + return jsonify({ + 'color': { + 'name': 'Guest Blue', + 'value': '#00ffff', + 'effect': None + } + }), 200 + + user = users_collection.find_one({'username': username}) + if not user: + # If it's not a guest but user is not found, return a default skin + return jsonify({ + 'color': { + 'name': 'Default Blue', + 'value': '#00ffff', + 'effect': None + } + }), 200 + + selected_skins = user.get('selected_skins', {}) + skin_data = {} + + for skin_type, skin_id in selected_skins.items(): + skin = skins_collection.find_one({'_id': skin_id}) + if skin: + skin_data[skin_type] = { + 'name': skin['name'], + 'value': skin['value'], + 'effect': skin['effect'] + } + + if not skin_data: + # Return default skin if no custom skin is found + skin_data = { + 'color': { + 'name': 'Default Blue', + 'value': '#00ffff', + 'effect': None + } + } + + # Generate ETag for caching + etag = generate_etag(json.dumps(skin_data, sort_keys=True).encode()) + + if request.if_none_match and etag in request.if_none_match: + return '', 304 + + response = make_response(jsonify(skin_data)) + response.set_etag(etag) + response.cache_control.max_age = 3600 # Cache for 1 hour + response.cache_control.public = True + return response, 200 + + +@socketio.on('start_audio') +def start_audio(): + audio_streamer.start() + +@socketio.on('stop_audio') +def stop_audio(): + audio_streamer.stop() + +@socketio.on('request_music_sync') +def on_request_music_sync(): + if request.sid in game_state['players'] and music_player.is_playing: + emit('music_sync', { + 'song': music_player.get_current_song(), + 'startTime': music_player.start_time, + 'serverTime': time.time(), + 'songDuration': music_player.get_current_song_duration() + }) + + +@socketio.on('connect') +def on_connect(): + print(f"Player {request.sid} connected") + active_sockets_collection.update_one( + {'_id': request.sid}, + {'$set': {'last_active': datetime.datetime.utcnow()}}, + upsert=True + ) + + +@socketio.on('disconnect') +def on_disconnect(): + player_id = request.sid + if player_id in game_state['players']: + player = game_state['players'][player_id] + username = player.name + del game_state['players'][player_id] + active_players.discard(username) + socketio.emit('player_disconnected', player_id, to=main_room) + send_discord_alert(f"👋 Player {username} has disconnected!") + print(f"Player {player_id} disconnected") + active_sockets_collection.delete_one({'_id': player_id}) + +@socketio.on('player_paused') +def on_player_paused(data): + player_id = request.sid + if player_id in game_state['players']: + player = game_state['players'][player_id] + player.is_paused = data['isPaused'] + emit('player_update', {'id': player_id, 'player': player.to_dict()}, to=main_room) + + +@socketio.on('join') +def on_join(data): + try: + player_id = request.sid + username = data.get('name') + player_class_id = data.get('class_id') + + # Check if the player is already in the game + if username in active_players: + emit('join_error', {'message': 'You are already playing in another window or device.'}) + return + + join_room(main_room) + + # Get player skin + user = users_collection.find_one({'username': username}) + if user: + selected_skins = user.get('selected_skins', {}) + skin_data = {} + for skin_type, skin_id in selected_skins.items(): + skin = skins_collection.find_one({'_id': skin_id}) + if skin: + skin_data[skin_type] = { + 'name': skin['name'], + 'value': skin['value'], + 'effect': skin['effect'] + } + else: + skin_data = { + 'color': { + 'name': 'Default Blue', + 'value': '#00ffff', + 'effect': None + } + } + + # If no class_id is provided or the provided class_id is invalid, select a random unlocked_by_default class + if not player_class_id or not classes_collection.find_one({'_id': ObjectId(player_class_id)}): + default_classes = list(classes_collection.find({'unlocked_by_default': True})) + if default_classes: + player_class = random.choice(default_classes) + player_class_id = str(player_class['_id']) + else: + emit('join_error', {'message': 'No default classes available'}) + return + else: + player_class = classes_collection.find_one({'_id': ObjectId(player_class_id)}) + + player_class['weapon_limit'] = int(player_class.get('weapon_limit', 1)) # Ensure this is an integer + + # Get starting weapon + weapon = weapons_collection.find_one({'_id': player_class['starting_weapon']}) + if weapon: + weapon['_id'] = str(weapon['_id']) # Convert ObjectId to string + else: + weapon = None # Provide a default if no weapon is found + + new_player = Player( + x=random.randint(50, 1000), + y=random.randint(50, 1000), + name=username, + skin=skin_data, + weapon=weapon, + weapons=[weapon] if weapon else [], # Add this line + player_class=player_class + ) + + new_player.weapons = [weapon] if weapon else [] + new_player.synth_coins = 0 + new_player.is_paused = False # Ensure the player starts unpaused + new_player.weapon_limit = player_class['weapon_limit'] + + game_state['players'][player_id] = new_player + active_players.add(username) + + serializable_game_state = serialize_game_state(game_state) + + if len(game_state['enemies']) < MAX_ENEMIES: + game_state['enemies'] += generate_enemies(MAX_ENEMIES - len(game_state['enemies'])) + + # Send game state to the new player + emit('game_state', json.loads(json.dumps(serializable_game_state, cls=CustomJSONEncoder)), to=player_id) + + # Notify all other players about the new player + emit('new_player', {'id': player_id, 'player': new_player.to_dict()}, to=main_room, include_self=False) + + emit('synth_coins_update', {'coins': [{'x': coin.x, 'y': coin.y} for coin in game_state['synth_coins']]}, to=player_id) + + # Then send music sync if music is playing + if music_player.is_playing: + emit('music_sync', { + 'song': music_player.get_current_song(), + 'startTime': music_player.start_time, + 'serverTime': time.time(), + 'songDuration': music_player.get_current_song_duration() + }) + send_discord_alert(f"👋 Player {username} has joined the game!") + print(f"Player {username} (ID: {player_id}) joined the game") + + except Exception as e: + logging.error(f"Error in on_join: {str(e)}") + emit('join_error', {'message': 'An error occurred while joining the game. Please try again.'}) + + +@socketio.on('player_update') +def player_update(data): + player_id = request.sid + if player_id in game_state['players']: + player = game_state['players'][player_id] + player.update(data) + serialized_player = json.loads(json.dumps(player.to_dict(), cls=CustomJSONEncoder)) + emit('player_update', {'id': player_id, 'player': serialized_player}, to=main_room) + +@socketio.on('collect_synth_coin') +def on_collect_synth_coin(data): + player_id = request.sid + if player_id in game_state['players']: + player = game_state['players'][player_id] + coin_x, coin_y = data['coin_x'], data['coin_y'] + + for coin in game_state['synth_coins'][:]: + if math.hypot(coin.x - coin_x, coin.y - coin_y) < 0.1: + player.synth_coins += 100 + game_state['synth_coins'].remove(coin) + print(f"Player {player.name} collected a coin. New count: {player.synth_coins}") + socketio.emit('coin_collected', { + 'player_id': player_id, + 'coin_count': player.synth_coins + }, room=player_id) # Send only to the player who collected the coin + break + + + +@socketio.on('player_shoot') +def player_shoot(data): + player_id = request.sid + if player_id in game_state['players']: + player = game_state['players'][player_id] + angle = data['angle'] + weapon_x = data['weaponX'] + weapon_y = data['weaponY'] + weapon_index = data.get('weaponIndex', 0) + if 0 <= weapon_index < len(player.weapons): + bullet = player.shoot(angle, weapon_x, weapon_y, weapon_index) + if bullet: + game_state['bullets'].append(bullet) + emit('new_bullet', bullet, room=main_room) + + + +@socketio.on('upgrade_selected') +def on_upgrade_selected(data): + player_id = request.sid + upgrade_index = data['upgradeIndex'] + upgrade_type = data['upgradeType'] + upgrade_value = data['upgradeValue'] + + print(f"Player {player_id} selected upgrade: Type: {upgrade_type}, Value: {upgrade_value}, Index: {upgrade_index}") + + if player_id in game_state['players']: + player = game_state['players'][player_id] + + print(f"Player {player.name} (ID: {player_id}) current weapon attributes:") + print(player.weapon['base_attributes']) + + if 'base_attributes' in player.weapon and upgrade_type in player.weapon['base_attributes']: + old_value = player.weapon['base_attributes'][upgrade_type] + player.weapon['base_attributes'][upgrade_type] += upgrade_value + new_value = player.weapon['base_attributes'][upgrade_type] + + # print(f"Upgrading {upgrade_type} for player {player.name}:") + # print(f" Old value: {old_value}") + # print(f" Upgrade value: {upgrade_value}") + # print(f" New value: {new_value}") + + # Notify the player about the upgrade + socketio.emit('upgrade_applied', { + 'attribute': upgrade_type, + 'value': upgrade_value, + 'new_value': new_value + }, room=player_id) + + player.is_paused = False + socketio.emit('player_update', {'id': player_id, 'player': player.to_dict()}, to=main_room) + + print(f"Player {player.name} (ID: {player_id}) updated weapon attributes:") + print(player.weapon['base_attributes']) + else: + error_msg = f"Upgrade attribute {upgrade_type} not found in player's weapon attributes" + print(f"Error: {error_msg}") + logging.error(error_msg) + else: + print(f"Error: Player ID {player_id} not found in game state") + +def add_discord_role(user_id, role_id): + url = f'https://discord.com/api/v10/guilds/{DISCORD_GUILD_ID}/members/{user_id}/roles/{role_id}' + headers = { + 'Authorization': f'Bot {DISCORD_BOT_TOKEN}', + 'Content-Type': 'application/json' + } + response = requests.put(url, headers=headers) + if response.status_code == 204: + print(f"Successfully added role to user {user_id}") + else: + print(f"Failed to add role to user {user_id}. Status code: {response.status_code}") + +def remove_discord_role(user_id, role_id): + url = f'https://discord.com/api/v10/guilds/{DISCORD_GUILD_ID}/members/{user_id}/roles/{role_id}' + headers = { + 'Authorization': f'Bot {DISCORD_BOT_TOKEN}', + 'Content-Type': 'application/json' + } + response = requests.delete(url, headers=headers) + if response.status_code == 204: + print(f"Successfully removed role from user {user_id}") + else: + print(f"Failed to remove role from user {user_id}. Status code: {response.status_code}") + + +def get_random_upgrades(count): + all_upgrades = [ + {'type': 'bullet_speed', 'name': 'Bullet Speed', 'value': 1}, + {'type': 'bullet_size', 'name': 'Bullet Size', 'value': 1}, + {'type': 'damage', 'name': 'Damage', 'value': 2}, + {'type': 'fire_rate', 'name': 'Fire Rate', 'value': 0.1} + ] + return random.sample(all_upgrades, count) + + +def cleanup_stale_sockets(): + stale_time = datetime.datetime.utcnow() - datetime.timedelta(minutes=5) + stale_sockets = active_sockets_collection.find({'last_active': {'$lt': stale_time}}) + + for socket in stale_sockets: + socketio.server.disconnect(socket['_id']) + active_sockets_collection.delete_one({'_id': socket['_id']}) + print(f"Cleaned up stale socket: {socket['_id']}") + + active_sockets_collection.delete_many({'last_active': {'$lt': stale_time}}) + + + +def add_experience(player, amount): + player.experience += amount + if player.experience >= player.max_experience: + player.level += 1 + player.experience -= player.max_experience + player.max_experience = int(player.max_experience * 1.2) + + # Generate upgrade options and store them + player_id = next(pid for pid, p in game_state['players'].items() if p == player) + game_state['player_upgrade_options'][player_id] = get_random_upgrades(3) + + return True + return False + +def update_synth_coins(players): + for coin in game_state['synth_coins'][:]: # Use a copy for iteration + closest_player = None + closest_distance = float('inf') + for player in players.values(): + if player.is_paused: + continue + distance = math.hypot(player.x - coin.x, player.y - coin.y) + if distance < coin.follow_radius and distance < closest_distance: + closest_player = player + closest_distance = distance + + if closest_player: + dx = closest_player.x - coin.x + dy = closest_player.y - coin.y + distance = math.hypot(dx, dy) + if distance > 0: + coin.x += (dx / distance) * coin.follow_speed + coin.y += (dy / distance) * coin.follow_speed + + # Check for collection + if distance <= closest_player.radius + coin.radius: + closest_player.synth_coins += 1 + game_state['synth_coins'].remove(coin) + socketio.emit('coin_collected', { + 'player_id': next(pid for pid, p in players.items() if p == closest_player), + 'coin_count': closest_player.synth_coins, + 'coin_x': coin.x, + 'coin_y': coin.y + }, room=main_room) + + + +def game_loop(): + global game_state + while True: + try: + current_time = time.time() + + # Update bullet positions and remove expired bullets + game_state['bullets'] = [bullet for bullet in game_state['bullets'] if current_time - bullet['created_at'] < BULLET_LIFETIME] + for bullet in game_state['bullets']: + bullet['x'] += math.cos(bullet['angle']) * bullet['speed'] + bullet['y'] += math.sin(bullet['angle']) * bullet['speed'] + + # Ensure enemies are spawned if there are not enough + if len(game_state['enemies']) < MAX_ENEMIES: + new_enemies = generate_enemies(MAX_ENEMIES - len(game_state['enemies'])) + game_state['enemies'].extend(new_enemies) + + # Update player health and check for collisions + for player_id, player in game_state['players'].items(): + if not player.is_paused: + # Update player position and other attributes + player.update({'x': player.x, 'y': player.y}) # Pass current position to maintain consistency + + # Check for collisions with enemies and apply damage + for enemy in game_state['enemies']: + distance = math.hypot(player.x - enemy['x'], player.y - enemy['y']) + # Check for damage to player + if distance < 30 + player.radius and not player.is_paused: + player.take_damage(enemy['attributes']['damage'], current_time) + + # If player's health reaches 0, handle player death + if player.current_health <= 0: + handle_player_death(player_id) + + + + # Update enemy positions and check for collisions + for enemy in game_state['enemies']: + if game_state['players']: + active_players = [p for p in game_state['players'].values() if not p.is_paused] + if active_players: + nearest_player = min(active_players, key=lambda p: math.hypot(p.x - enemy['x'], p.y - enemy['y'])) + dx = nearest_player.x - enemy['x'] + dy = nearest_player.y - enemy['y'] + distance = math.hypot(dx, dy) + + # Calculate new position + new_x = enemy['x'] + new_y = enemy['y'] + if distance > 0: + new_x += (dx / distance) * enemy['attributes']['speed'] + new_y += (dy / distance) * enemy['attributes']['speed'] + + # Check collision with other enemies + for other_enemy in game_state['enemies']: + if other_enemy != enemy: + collision_distance = math.hypot(new_x - other_enemy['x'], new_y - other_enemy['y']) + if collision_distance < 30: # Assuming enemy size is 30 + # Calculate bounce direction + bounce_dx = new_x - other_enemy['x'] + bounce_dy = new_y - other_enemy['y'] + bounce_distance = math.hypot(bounce_dx, bounce_dy) + if bounce_distance > 0: + new_x = other_enemy['x'] + (bounce_dx / bounce_distance) * 30 + new_y = other_enemy['y'] + (bounce_dy / bounce_distance) * 30 + + # Check collision with players + for player in active_players: + collision_distance = math.hypot(new_x - player.x, new_y - player.y) + if collision_distance < 30 + player.radius: # Assuming enemy size is 30 + # Calculate bounce direction + bounce_dx = new_x - player.x + bounce_dy = new_y - player.y + bounce_distance = math.hypot(bounce_dx, bounce_dy) + if bounce_distance > 0: + new_x = player.x + (bounce_dx / bounce_distance) * (30 + player.radius) + new_y = player.y + (bounce_dy / bounce_distance) * (30 + player.radius) + + # Update enemy position + enemy['x'] = new_x + enemy['y'] = new_y + + # Check for collisions between bullets and enemies + for bullet in game_state['bullets'][:]: + for enemy in game_state['enemies'][:]: + if math.hypot(bullet['x'] - enemy['x'], bullet['y'] - enemy['y']) < 30 + bullet['size']: + damage_dealt = bullet['damage'] + enemy['current_health'] -= damage_dealt + game_state['bullets'].remove(bullet) + + # Generate impact effect data + impact_effect = { + 'x': bullet['x'], + 'y': bullet['y'], + 'color': bullet['color'], # Use the bullet's color + 'size': bullet['size'], + 'angle': bullet['angle'] + } + + # Emit impact effect to all clients + socketio.emit('bullet_impact', impact_effect, room=main_room) + + # Emit damage_dealt event + socketio.emit('damage_dealt', { + 'damage': damage_dealt, + 'enemyX': enemy['x'], + 'enemyY': enemy['y'] + }, room=bullet['player_id']) + + if enemy['current_health'] <= 0: + game_state['enemies'].remove(enemy) + + coin = SynthCoin(enemy['x'], enemy['y']) + game_state['synth_coins'].append(coin) + socketio.emit('new_synth_coin', {'x': coin.x, 'y': coin.y}, room=main_room) + + # Grant experience to the player who shot the bullet + player_id = bullet.get('player_id') + if player_id and player_id in game_state['players']: + player = game_state['players'][player_id] + exp_gained = enemy['experience'] # Use the enemy's experience value + leveled_up = add_experience(player, exp_gained) + socketio.emit('experience_update', { + 'player_id': player_id, + 'experience': player.experience, + 'max_experience': player.max_experience, + 'level': player.level, + 'leveled_up': leveled_up, + 'exp_gained': exp_gained + }, room=main_room) + + # Generate particles + particles = [ + { + 'x': enemy['x'], + 'y': enemy['y'], + 'color': 'red', # You can change this to match the enemy color if needed + 'radius': random.uniform(2, 5), + 'velocityX': random.uniform(-2, 2), + 'velocityY': random.uniform(-2, 2), + 'ttl': 60 + } for _ in range(10) + ] + socketio.emit('enemy_destroyed', {'particles': particles}, room=main_room) + + # Spawn a new enemy if needed + if len(game_state['enemies']) < MAX_ENEMIES: + game_state['enemies'].extend(generate_enemies(1)) + break + + # Update player shooting + # Update player shooting + for player_id, player in game_state['players'].items(): + if not player.is_paused: + update_synth_coins(game_state['players']) + if player.auto_fire_enabled: # Only auto-fire if enabled + nearest_enemy = find_nearest_enemy(player) + if nearest_enemy: + target_angle = math.atan2(nearest_enemy['y'] - player.y, nearest_enemy['x'] - player.x) + for i, weapon in enumerate(player.weapons): + player.weapon_angles[i] = lerp_angle(player.weapon_angles[i], target_angle, 0.1) + if current_time - player.last_shoot_times[i] > 1 / weapon['base_attributes']['fire_rate']: + weapon_x = player.x + math.cos(player.weapon_angles[i]) * player.orbit_radius + weapon_y = player.y + math.sin(player.weapon_angles[i]) * player.orbit_radius + bullet = player.shoot(player.weapon_angles[i], weapon_x, weapon_y, i) + if bullet: + game_state['bullets'].append(bullet) + socketio.emit('new_bullet', bullet, room=main_room) + # Emit updated weapon angles + socketio.emit('player_update', {'id': player_id, 'player': player.to_dict()}, room=main_room) + + # Send current music state to the new player + if game_state['is_playing']: + song_duration = len(AudioSegment.from_mp3(game_state['playlist'][game_state['current_song_index']])) + if current_time - game_state['music_start_time'] > song_duration / 1000: + change_song() + + if music_player.is_playing: + if music_player.update(): + # Song has changed, notify clients + socketio.emit('music_control', { + 'action': 'change', + 'song': f"/{music_player.get_current_song()}", + 'startTime': music_player.start_time, + 'songDuration': music_player.get_current_song_duration() + }, room=main_room) + + game_state['synth_coins'] = [coin for coin in game_state['synth_coins'] if current_time - coin.created_at < 300] + # Emit game update + serialized_state = serialize_game_state(game_state) + serialized_state['synth_coins'] = [{'x': coin.x, 'y': coin.y} for coin in game_state['synth_coins']] + socketio.emit('game_update', serialized_state, room=main_room) + + socketio.sleep(0.033) # Update approximately 30 times per second + except Exception as e: + logging.error(f"Error in game loop: {str(e)}") + break + +def lerp_angle(a, b, t): + diff = b - a + adjusted = ((diff + math.pi) % (2 * math.pi)) - math.pi + return a + adjusted * t + +def handle_player_death(player_id): + player = game_state['players'][player_id] + # Implement death logic here, for example: + # - Respawn the player + # - Reset their health + # - Apply any death penalties + player.x = random.randint(50, MAP_WIDTH - 50) + player.y = random.randint(50, MAP_HEIGHT - 50) + player.current_health = player.max_health + socketio.emit('player_died', {'player_id': player_id}, room=main_room) + + +def find_nearest_enemy(player): + nearest_enemy = None + nearest_distance = float('inf') + min_targeting_distance = player.orbit_radius + player.targeting_buffer + + for enemy in game_state['enemies']: + distance = math.hypot(enemy['x'] - player.x, enemy['y'] - player.y) + if min_targeting_distance <= distance < nearest_distance: + nearest_distance = distance + nearest_enemy = enemy + + return nearest_enemy + + +def create_bullet(player, angle, weapon_x, weapon_y, weapon): + bullet = { + 'x': weapon_x, + 'y': weapon_y, + 'angle': angle, + 'speed': weapon['base_attributes']['bullet_speed'], + 'size': weapon['base_attributes']['bullet_size'], + 'damage': weapon['base_attributes']['damage'] * player.damage_multiplier, + 'created_at': time.time(), + 'player_id': next(pid for pid, p in game_state['players'].items() if p == player), + 'weapon_type': weapon['weapon_type'], + 'bullet_image_source': weapon['bullet_image_source'], + 'color': weapon.get('bullet_color', '#ffff00') # Default to yellow if no color specified + } + return bullet + + +# Admin console commands +def start_music(): + music_player.start() + socketio.emit('music_control', { + 'action': 'start', + 'song': music_player.get_current_song(), + 'startTime': music_player.start_time, + 'songDuration': music_player.get_current_song_duration() + }, room=main_room) + +def stop_music(): + music_player.stop() + socketio.emit('music_stop', room=main_room) + +def change_song(): + new_song = music_player.next_song() + socketio.emit('music_control', { + 'action': 'change', + 'song': new_song, + 'startTime': music_player.start_time, + 'songDuration': music_player.get_current_song_duration() + }, room=main_room) + + +class AudioStreamer: + def __init__(self, music_player): + self.music_player = music_player + self.current_position = 0 + self.is_playing = False + + def get_next_chunk(self): + if self.current_position >= len(self.music_player.current_audio): + if self.music_player.update(): + self.current_position = 0 + else: + return None + + chunk = self.music_player.current_audio[self.current_position:self.current_position + CHUNK_SIZE] + self.current_position += CHUNK_SIZE + return chunk + + def start(self): + self.is_playing = True + + def stop(self): + self.is_playing = False + +# Initialize the audio streamer + +class MusicPlayer: + def __init__(self, playlist): + self.playlist = playlist + self.current_song_index = 0 + self.start_time = None + self.is_playing = False + self.song_durations = self.get_song_durations() + self.current_audio = None + + def get_song_durations(self): + durations = {} + for song in self.playlist: + try: + durations[os.path.basename(song)] = len(AudioSegment.from_mp3(song)) / 1000 + except Exception as e: + logging.error(f"Error getting duration for {song}: {str(e)}") + durations[os.path.basename(song)] = 180 # Default to 3 minutes if there's an error + return durations + + def start(self): + self.is_playing = True + self.start_time = time.time() + self.load_current_song() + send_discord_alert(f"🎵 Now playing: {self.get_current_song()}") + + + def stop(self): + self.is_playing = False + self.start_time = None + + def get_current_song(self): + # Return just the filename, not the full path + return os.path.basename(self.playlist[self.current_song_index]) + + def get_current_song_duration(self): + return self.song_durations[self.get_current_song()] + + def load_current_song(self): + self.current_audio = AudioSegment.from_mp3(self.playlist[self.current_song_index]) + + def update(self): + if self.is_playing: + current_time = time.time() + elapsed_time = current_time - self.start_time + current_song_duration = self.get_current_song_duration() + if elapsed_time > current_song_duration: + self.current_song_index = (self.current_song_index + 1) % len(self.playlist) + self.start_time = current_time + self.load_current_song() + return True # Indicates that the song has changed + return False + + def next_song(self): + self.current_song_index = (self.current_song_index + 1) % len(self.playlist) + self.start_time = time.time() + self.load_current_song() + send_discord_alert(f"🎵 Now playing: {self.get_current_song()}") + return self.get_current_song() + + + + def change_song(self): + self.current_song_index = (self.current_song_index + 1) % len(self.playlist) + self.start_time = time.time() + self.load_current_song() + send_discord_alert(f"🎵 Now playing: {self.get_current_song()}") + + def reload_playlist(self): + new_playlist = load_music_playlist() + + # Find the current song in the new playlist + current_song = self.playlist[self.current_song_index] + if current_song in new_playlist: + self.current_song_index = new_playlist.index(current_song) + else: + self.current_song_index = 0 # Reset to the first song if current song not found + + self.playlist = new_playlist + self.song_durations = self.get_song_durations() + + return len(self.playlist) + + +# Initialize the music player with the dynamically loaded playlist +music_player = MusicPlayer(load_music_playlist()) +audio_streamer = AudioStreamer(music_player) + +class Player: + def __init__(self, x, y, name, skin, weapon, weapons, player_class): + self.x = x + self.y = y + self.radius = 20 + self.targeting_buffer = 10 + self.name = name + self.skin = skin + self.weapon = weapon + self.weapons = [weapon] if weapon else [] + self.weapon_angles = [0] * len(weapons) + self.last_shoot_times = [0] * len(weapons) + self.weapon_limit = int(player_class.get('weapon_limit', 1)) # Ensure this is an integer + self.weapon_angle = 0 + self.orbit_radius = 40 + self.player_class = player_class + self.health = player_class['base_attributes']['health'] + self.speed = player_class['base_attributes']['speed'] + self.damage_multiplier = player_class['base_attributes']['damage_multiplier'] + self.level = 1 + self.experience = 0 + self.max_experience = 100 + self.last_shot_time = 0 + self.is_paused = False + self.synth_coins = 0 + self.max_health = player_class['base_attributes']['health'] + self.current_health = self.max_health + self.health_regen = player_class['base_attributes'].get('health_regen', 1) # Default to 1 if not specified + self.invincibility_duration = 1 # 1 second of invincibility after taking damage + self.last_damage_time = 0 + self.regen_cooldown = 5 # 5 seconds cooldown before health regeneration starts + self.auto_fire_enabled = False # Default to False + + + def to_dict(self): + return { + 'x': self.x, + 'y': self.y, + 'name': self.name, + 'skin': self.skin, + 'weapon': self._serialize_dict(self.weapon), + 'weapon_angle': self.weapon_angle, + 'player_class': self._serialize_dict(self.player_class), + 'health': self.health, + 'speed': self.speed, + 'damage_multiplier': self.damage_multiplier, + 'level': self.level, + 'experience': self.experience, + 'max_experience': self.max_experience, + 'last_shot_time': self.last_shot_time, + 'is_paused': self.is_paused, + 'weapons': [self._serialize_dict(w) for w in self.weapons], + 'weapon_angles': self.weapon_angles, + 'weapon_limit': self.weapon_limit, + 'current_health': self.current_health, + 'max_health': self.max_health, + 'auto_fire_enabled': self.auto_fire_enabled + + } + + def add_weapon(self, weapon): + if len(self.weapons) < self.weapon_limit: + self.weapons.append(weapon) + self.weapon_angles.append(0) + self.last_shoot_times.append(0) + + + def shoot(self, angle, weapon_x, weapon_y, weapon_index): + if weapon_index < 0 or weapon_index >= len(self.weapons): + return None + + weapon = self.weapons[weapon_index] + current_time = time.time() + if current_time - self.last_shoot_times[weapon_index] > 1 / weapon['base_attributes']['fire_rate']: + bullet = create_bullet(self, angle, weapon_x, weapon_y, weapon) + self.last_shoot_times[weapon_index] = current_time + return bullet + return None + + + + def _serialize_dict(self, d): + """Helper method to serialize dictionaries containing ObjectId""" + if isinstance(d, dict): + return {k: str(v) if isinstance(v, ObjectId) else self._serialize_dict(v) for k, v in d.items()} + elif isinstance(d, list): + return [self._serialize_dict(v) for v in d] + elif isinstance(d, ObjectId): + return str(d) + else: + return d + + + + def update(self, data): + current_time = time.time() + # Update player attributes based on the data received + if 'x' in data: + self.x = data['x'] + if 'y' in data: + self.y = data['y'] + # Add other attributes as needed + if 'weapon_angle' in data: + self.weapon_angle = data['weapon_angle'] + if 'weapon_angles' in data: + self.weapon_angles = data['weapon_angles'] + if current_time - self.last_damage_time > self.regen_cooldown: + if not self.is_paused: + self.regenerate_health() + if 'auto_fire_enabled' in data: + self.auto_fire_enabled = data['auto_fire_enabled'] + + + def take_damage(self, damage, current_time): + if current_time - self.last_damage_time > self.invincibility_duration: + self.current_health -= damage + self.current_health = max(0, self.current_health) # Ensure health doesn't go below 0 + self.last_damage_time = current_time + + def regenerate_health(self): + self.current_health = min(self.current_health + self.health_regen, self.max_health) + + + + + + def add_experience(self, amount): + self.experience += amount + leveled_up = False + while self.experience >= self.max_experience: + self.level += 1 + self.experience -= self.max_experience + self.max_experience = int(self.max_experience * 1.2) + leveled_up = True + return leveled_up + + + + + + + +def stream_audio(): + while True: + if audio_streamer.is_playing: + chunk = audio_streamer.get_next_chunk() + buffer = io.BytesIO() + chunk.export(buffer, format="mp3") + audio_data = buffer.getvalue() + socketio.emit('audio_chunk', {'data': audio_data}, room=main_room) + socketio.sleep(CHUNK_SIZE / 44100) # Sleep for the duration of the chunk + + +class AdminConsole(cmd.Cmd): + prompt = 'game_admin> ' + + def do_start_music(self, arg): + """Start the music""" + start_music() + + def do_stop_music(self, arg): + """Stop the music""" + stop_music() + + def do_change_song(self, arg): + """Change the current song""" + if not arg: + print("Error: Song filename is required") + else: + change_song(arg) + + def do_quit(self, arg): + """Exit the admin console""" + print("Exiting admin console") + return True + + def do_next_song(self, arg): + """Change to the next song in the playlist""" + new_song = music_player.next_song() + print(f"Changed to next song: {new_song}") + socketio.emit('music_control', { + 'action': 'change', + 'song': new_song, + 'startTime': music_player.start_time, + 'songDuration': music_player.get_current_song_duration() + }, room=main_room) + + def do_send_alert(self, arg): + """Send an alert to all players: send_alert """ + if arg: + send_alert(arg) + print(f"Alert sent: {arg}") + send_discord_alert(arg) + else: + print("Error: Please provide a message for the alert") + + + def do_reload_playlist(self, arg): + """Reload the music playlist""" + song_count = music_player.reload_playlist() + print(f"Playlist reloaded. {song_count} songs available.") + + # Optionally, you can change to the next song after reloading + new_song = music_player.next_song() + print(f"Changed to next song: {new_song}") + socketio.emit('music_control', { + 'action': 'change', + 'song': new_song, + 'startTime': music_player.start_time, + 'songDuration': music_player.get_current_song_duration() + }, room=main_room) + + + +def admin_cli(): + try: + AdminConsole().cmdloop() + except Exception as e: + logging.error(f"Error in admin console: {str(e)}") + + +if __name__ == '__main__': + cleanup_stale_sockets() + + cli_thread = threading.Thread(target=admin_cli) + cli_thread.daemon = True + cli_thread.start() + + # Start the music automatically + start_music() + + game_loop_thread = socketio.start_background_task(game_loop) + audio_stream_thread = socketio.start_background_task(stream_audio) + + try: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain( + 'C:/Certbot/live/resonancerumble.com/fullchain.pem', + 'C:/Certbot/live/resonancerumble.com/privkey.pem' + ) + send_discord_alert("🚀 Resonance Rumble server has started!") + socketio.run(app, host='0.0.0.0', port=443, ssl_context=ssl_context) + except KeyboardInterrupt: + logging.info("Server shutting down...") + finally: + # Perform cleanup + logging.info("Cleaning up resources...") + if 'client' in globals(): + client.close() + socketio.stop() + diff --git a/http_redirect.py b/http_redirect.py new file mode 100644 index 0000000..5dcffe6 --- /dev/null +++ b/http_redirect.py @@ -0,0 +1,12 @@ +from flask import Flask, redirect, request + +app = Flask(__name__) + +@app.route('/', defaults={'path': ''}) +@app.route('/') +def catch_all(path): + return redirect(f"https://resonancerumble.com/{path}", code=301) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=80) + diff --git a/index.html b/index.html new file mode 100644 index 0000000..022d3fa --- /dev/null +++ b/index.html @@ -0,0 +1,187 @@ + + + + + + Resonance Rumble + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + +
+
+ + + +
+
Welcome to Resonance Rumble
+ + +
+ + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/music/DVRST-CloseEyes.mp3 b/music/DVRST-CloseEyes.mp3 new file mode 100644 index 0000000..f31c773 Binary files /dev/null and b/music/DVRST-CloseEyes.mp3 differ diff --git a/music/HOME-Resonance.mp3 b/music/HOME-Resonance.mp3 new file mode 100644 index 0000000..4f5cc94 Binary files /dev/null and b/music/HOME-Resonance.mp3 differ diff --git a/music/MFGhost-RememberMe.mp3 b/music/MFGhost-RememberMe.mp3 new file mode 100644 index 0000000..cdd7d03 Binary files /dev/null and b/music/MFGhost-RememberMe.mp3 differ diff --git a/music/MackyGee-Tour.mp3 b/music/MackyGee-Tour.mp3 new file mode 100644 index 0000000..7e43d3f Binary files /dev/null and b/music/MackyGee-Tour.mp3 differ diff --git a/music/deadmau5-Quezacotl.mp3 b/music/deadmau5-Quezacotl.mp3 new file mode 100644 index 0000000..89c7675 Binary files /dev/null and b/music/deadmau5-Quezacotl.mp3 differ diff --git a/music/deadmau5-TheVeldt.mp3 b/music/deadmau5-TheVeldt.mp3 new file mode 100644 index 0000000..e2b0cc0 Binary files /dev/null and b/music/deadmau5-TheVeldt.mp3 differ diff --git a/node_modules/.bin/tauri b/node_modules/.bin/tauri new file mode 100644 index 0000000..546529b --- /dev/null +++ b/node_modules/.bin/tauri @@ -0,0 +1,16 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir=`cygpath -w "$basedir"` + fi + ;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../@tauri-apps/cli/tauri.js" "$@" +else + exec node "$basedir/../@tauri-apps/cli/tauri.js" "$@" +fi diff --git a/node_modules/.bin/tauri.cmd b/node_modules/.bin/tauri.cmd new file mode 100644 index 0000000..534b08c --- /dev/null +++ b/node_modules/.bin/tauri.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@tauri-apps\cli\tauri.js" %* diff --git a/node_modules/.bin/tauri.ps1 b/node_modules/.bin/tauri.ps1 new file mode 100644 index 0000000..01b0ad7 --- /dev/null +++ b/node_modules/.bin/tauri.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../@tauri-apps/cli/tauri.js" $args + } else { + & "$basedir/node$exe" "$basedir/../@tauri-apps/cli/tauri.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../@tauri-apps/cli/tauri.js" $args + } else { + & "node$exe" "$basedir/../@tauri-apps/cli/tauri.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..0d392b0 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "resonancerumble", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@tauri-apps/cli": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.0.tgz", + "integrity": "sha512-DBBpBl6GhTzm8ImMbKkfaZ4fDTykWrC7Q5OXP4XqD91recmDEn2LExuvuiiS3HYe7uP8Eb5B9NPHhqJb+Zo7qQ==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "1.6.0", + "@tauri-apps/cli-darwin-x64": "1.6.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.6.0", + "@tauri-apps/cli-linux-arm64-gnu": "1.6.0", + "@tauri-apps/cli-linux-arm64-musl": "1.6.0", + "@tauri-apps/cli-linux-x64-gnu": "1.6.0", + "@tauri-apps/cli-linux-x64-musl": "1.6.0", + "@tauri-apps/cli-win32-arm64-msvc": "1.6.0", + "@tauri-apps/cli-win32-ia32-msvc": "1.6.0", + "@tauri-apps/cli-win32-x64-msvc": "1.6.0" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.0.tgz", + "integrity": "sha512-h54FHOvGi7+LIfRchzgZYSCHB1HDlP599vWXQQJ/XnwJY+6Rwr2E5bOe/EhqoG8rbGkfK0xX3KPAvXPbUlmggg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/node_modules/@tauri-apps/cli-win32-x64-msvc/README.md b/node_modules/@tauri-apps/cli-win32-x64-msvc/README.md new file mode 100644 index 0000000..c22490b --- /dev/null +++ b/node_modules/@tauri-apps/cli-win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@tauri-apps/cli` diff --git a/node_modules/@tauri-apps/cli-win32-x64-msvc/cli.win32-x64-msvc.node b/node_modules/@tauri-apps/cli-win32-x64-msvc/cli.win32-x64-msvc.node new file mode 100644 index 0000000..2648200 Binary files /dev/null and b/node_modules/@tauri-apps/cli-win32-x64-msvc/cli.win32-x64-msvc.node differ diff --git a/node_modules/@tauri-apps/cli/CHANGELOG.md b/node_modules/@tauri-apps/cli/CHANGELOG.md new file mode 100644 index 0000000..21bd81c --- /dev/null +++ b/node_modules/@tauri-apps/cli/CHANGELOG.md @@ -0,0 +1,545 @@ +# Changelog + +## \[1.6.0] + +### New Features + +- [`253595a22`](https://www.github.com/tauri-apps/tauri/commit/253595a22d8659a1cb199bfc423e988ea82191e6) ([#9809](https://www.github.com/tauri-apps/tauri/pull/9809)) Add RPM packaging +- [`a301be52d`](https://www.github.com/tauri-apps/tauri/commit/a301be52d276f1e99316d23b4f0a8e458e29bc35) ([#9914](https://www.github.com/tauri-apps/tauri/pull/9914)) Use cargo's target directory to store and cache bundling tools when `bundle > useLocalToolsDir` option is active. + +### Bug Fixes + +- [`08f57efef`](https://www.github.com/tauri-apps/tauri/commit/08f57efefdd6dac10277bfc5f71eba0ca84a64c4) ([#10136](https://www.github.com/tauri-apps/tauri/pull/10136)) Fix parsing of cargo profile when using `--profile=` syntax. +- [`674accad7`](https://www.github.com/tauri-apps/tauri/commit/674accad75fccac6f9adc515a863f9d59efbee57) ([#10015](https://www.github.com/tauri-apps/tauri/pull/10015)) Add missing dependency `libayatana-appindicator3.so.1` for rpm package. + +### Dependencies + +- Upgraded to `tauri-cli@1.6.0` + +## \[1.5.14] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.14` + +## \[1.5.13] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.13` + +## \[1.5.12] + +### Bug Fixes + +- [`1675e41f0`](https://www.github.com/tauri-apps/tauri/commit/1675e41f05c77d517890f59fddcf536744e6a0ad)([#9481](https://www.github.com/tauri-apps/tauri/pull/9481)) Fixed an issue with the CLI renaming the main executable in kebab-case when building for Windows on a non-Windows system which caused the bundler step to fail. +- [`9dd67abd9`](https://www.github.com/tauri-apps/tauri/commit/9dd67abd93e96097fc169404b70e729e46c3cd64)([#9298](https://www.github.com/tauri-apps/tauri/pull/9298)) Upgrade `heck` to v0.5 to better support Chinese and Japanese product name, because Chinese do not have word separation. +- [`f9638b631`](https://www.github.com/tauri-apps/tauri/commit/f9638b6315668ced871f242224f001f474262f85)([#9491](https://www.github.com/tauri-apps/tauri/pull/9491)) Fixed an issue that caused the CLI to rename app binaries incorrectly if the product name contained a `.` which resulted in the bundling step to fail. +- [`77cc49ac3`](https://www.github.com/tauri-apps/tauri/commit/77cc49ac3cd27368b3be4f67e35ae021acee4c92)([#9188](https://www.github.com/tauri-apps/tauri/pull/9188)) Fixed an issue causing the `build.runner` and `build.features` configs to not take effect. +- [`aeddc40b9`](https://www.github.com/tauri-apps/tauri/commit/aeddc40b9e461bc118382ae62431d39e29f25915)([#9411](https://www.github.com/tauri-apps/tauri/pull/9411)) Fix `tauri info` crashing when Node.js is not installed. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.12` + +## \[1.5.11] + +### Bug Fixes + +- [`b15948b11`](https://www.github.com/tauri-apps/tauri/commit/b15948b11c0e362eea7ef57a4606f15f7dbd886b)([#8903](https://www.github.com/tauri-apps/tauri/pull/8903)) Fix `.taurignore` failing to ignore in some cases. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.11` + +## \[1.5.10] + +### Bug Fixes + +- [`b0f27814`](https://www.github.com/tauri-apps/tauri/commit/b0f27814b90ded2f1ed44b7852080eedbff0d9e4)([#8776](https://www.github.com/tauri-apps/tauri/pull/8776)) Fix `fail to rename app` when using `--profile dev`. +- [`0bff8c32`](https://www.github.com/tauri-apps/tauri/commit/0bff8c325d004fdead2023f58e0f5fd73a9c22ba)([#8697](https://www.github.com/tauri-apps/tauri/pull/8697)) Fix the built-in dev server failing to serve files when URL had queries `?` and other url components. +- [`67d7877f`](https://www.github.com/tauri-apps/tauri/commit/67d7877f27f265c133a70d48a46c83ffff31d571)([#8520](https://www.github.com/tauri-apps/tauri/pull/8520)) The cli now also watches cargo workspace members if the tauri folder is the workspace root. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.10` + +## \[1.5.9] + +### Bug Fixes + +- [`0a2175ea`](https://www.github.com/tauri-apps/tauri/commit/0a2175eabb736b2a4cd01ab682e08be0b5ebb2b9)([#8439](https://www.github.com/tauri-apps/tauri/pull/8439)) Expand glob patterns in workspace member paths so the CLI would watch all matching pathhs. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.9` + +## \[1.5.8] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.8` + +## \[1.5.7] + +### Bug Fixes + +- [`1d5aa38a`](https://www.github.com/tauri-apps/tauri/commit/1d5aa38ae418ea31f593590b6d32cf04d3bfd8c1)([#8162](https://www.github.com/tauri-apps/tauri/pull/8162)) Fixes errors on command output, occuring when the output stream contains an invalid UTF-8 character, or ends with a multi-bytes UTF-8 character. +- [`f26d9f08`](https://www.github.com/tauri-apps/tauri/commit/f26d9f0884f63f61b9f4d4fac15e6b251163793e)([#8263](https://www.github.com/tauri-apps/tauri/pull/8263)) Fixes an issue in the NSIS installer which caused the uninstallation to leave empty folders on the system if the `resources` feature was used. +- [`92bc7d0e`](https://www.github.com/tauri-apps/tauri/commit/92bc7d0e16157434330a1bcf1eefda6f0f1e5f85)([#8233](https://www.github.com/tauri-apps/tauri/pull/8233)) Fixes an issue in the NSIS installer which caused the installation to take much longer than expected when many `resources` were added to the bundle. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.7` + +## \[1.5.6] + +### Bug Fixes + +- [`5264e41d`](https://www.github.com/tauri-apps/tauri/commit/5264e41db3763e4c2eb0c3c21bd423fb7bece3e2)([#8082](https://www.github.com/tauri-apps/tauri/pull/8082)) Downgraded `rust-minisign` to `0.7.3` to fix signing updater bundles with empty passwords. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.6` + +## \[1.5.5] + +### Enhancements + +- [`9bead42d`](https://www.github.com/tauri-apps/tauri/commit/9bead42dbca0fb6dd7ea0b6bfb2f2308a5c5f992)([#8059](https://www.github.com/tauri-apps/tauri/pull/8059)) Allow rotating the updater private key. + +### Bug Fixes + +- [`be8e5aa3`](https://www.github.com/tauri-apps/tauri/commit/be8e5aa3071d9bc5d0bd24647e8168f312d11c8d)([#8042](https://www.github.com/tauri-apps/tauri/pull/8042)) Fixes duplicated newlines on command outputs. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.5` + +## \[1.5.4] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.4` + +## \[1.5.3] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.3` + +## \[1.5.2] + +### Dependencies + +- Upgraded to `tauri-cli@1.5.2` + +## \[1.5.1] + +### Bug Fixes + +- [`d6eb46cf`](https://www.github.com/tauri-apps/tauri/commit/d6eb46cf1116d147121f6b6db9d390b5e2fb238d)([#7934](https://www.github.com/tauri-apps/tauri/pull/7934)) On macOS, fix the `apple-id` option name when using `notarytools submit`. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.1` + +## \[1.5.0] + +### New Features + +- [`e1526626`](https://www.github.com/tauri-apps/tauri/commit/e152662687ece7a62d383923a50751cc0dd34331)([#7723](https://www.github.com/tauri-apps/tauri/pull/7723)) Support Bun package manager in CLI + +### Enhancements + +- [`13279917`](https://www.github.com/tauri-apps/tauri/commit/13279917d4cae071d0ce3a686184d48af079f58a)([#7713](https://www.github.com/tauri-apps/tauri/pull/7713)) Add version of Rust Tauri CLI installed with Cargo to `tauri info` command. + +### Bug Fixes + +- [`dad4f54e`](https://www.github.com/tauri-apps/tauri/commit/dad4f54eec9773d2ea6233a7d9fd218741173823)([#7277](https://www.github.com/tauri-apps/tauri/pull/7277)) Removed the automatic version check of the CLI that ran after `tauri` commands which caused various issues. + +### Dependencies + +- Upgraded to `tauri-cli@1.5.0` + +## \[1.4.0] + +### New Features + +- [`0ddbb3a1`](https://www.github.com/tauri-apps/tauri/commit/0ddbb3a1dc1961ba5c6c1a60081513c1380c8af1)([#7015](https://www.github.com/tauri-apps/tauri/pull/7015)) Provide prebuilt CLIs for Windows ARM64 targets. +- [`35cd751a`](https://www.github.com/tauri-apps/tauri/commit/35cd751adc6fef1f792696fa0cfb471b0bf99374)([#5176](https://www.github.com/tauri-apps/tauri/pull/5176)) Added the `desktop_template` option on `tauri.conf.json > tauri > bundle > deb`. +- [`6c5ade08`](https://www.github.com/tauri-apps/tauri/commit/6c5ade08d97844bb685789d30e589400bbe3e04c)([#4537](https://www.github.com/tauri-apps/tauri/pull/4537)) Added `tauri completions` to generate shell completions scripts. +- [`e092f799`](https://www.github.com/tauri-apps/tauri/commit/e092f799469ff32c7d1595d0f07d06fd2dab5c29)([#6887](https://www.github.com/tauri-apps/tauri/pull/6887)) Add `nsis > template` option to specify custom NSIS installer template. + +### Enhancements + +- [`d75c1b82`](https://www.github.com/tauri-apps/tauri/commit/d75c1b829bd96d9e3a672bcc79120597d5ada4a0)([#7181](https://www.github.com/tauri-apps/tauri/pull/7181)) Print a useful error when `updater` bundle target is specified without an updater-enabled target. +- [`52474e47`](https://www.github.com/tauri-apps/tauri/commit/52474e479d695865299d8c8d868fb98b99731020)([#7141](https://www.github.com/tauri-apps/tauri/pull/7141)) Enhance injection of Cargo features. +- [`2659ca1a`](https://www.github.com/tauri-apps/tauri/commit/2659ca1ab4799a5bda65c229c149e98bd01eb1ee)([#6900](https://www.github.com/tauri-apps/tauri/pull/6900)) Add `rustls` as default Cargo feature. + +### Bug Fixes + +- [`3cb7a3e6`](https://www.github.com/tauri-apps/tauri/commit/3cb7a3e642bb10ee90dc1d24daa48b8c8c15c9ce)([#6997](https://www.github.com/tauri-apps/tauri/pull/6997)) Fix built-in devserver adding hot-reload code to non-html files. +- [`fb7ef8da`](https://www.github.com/tauri-apps/tauri/commit/fb7ef8dacd9ade96976c84d22507782cdaf38acf)([#6667](https://www.github.com/tauri-apps/tauri/pull/6667)) Fix nodejs binary regex when `0` is in the version name, for example `node-20` +- [`1253bbf7`](https://www.github.com/tauri-apps/tauri/commit/1253bbf7ae11a87887e0b3bd98cc26dbb98c8130)([#7013](https://www.github.com/tauri-apps/tauri/pull/7013)) Fixes Cargo.toml feature rewriting. + +## \[1.3.1] + +- Correctly escape XML for resource files in WiX bundler. + - Bumped due to a bump in tauri-bundler. + - Bumped due to a bump in cli.rs. + - [6a6b1388](https://www.github.com/tauri-apps/tauri/commit/6a6b1388ea5787aea4c0e0b0701a4772087bbc0f) fix(bundler): correctly escape resource xml, fixes [#6853](https://www.github.com/tauri-apps/tauri/pull/6853) ([#6855](https://www.github.com/tauri-apps/tauri/pull/6855)) on 2023-05-04 + +- Added the following languages to the NSIS bundler: + +- `Spanish` + +- `SpanishInternational` + +- Bumped due to a bump in tauri-bundler. + - Bumped due to a bump in cli.rs. + +- [422b4817](https://www.github.com/tauri-apps/tauri/commit/422b48179856504e980a156500afa8e22c44d357) Add Spanish and SpanishInternational languages ([#6871](https://www.github.com/tauri-apps/tauri/pull/6871)) on 2023-05-06 + +- Correctly escape arguments in NSIS script to fix bundling apps that use non-default WebView2 install modes. + - Bumped due to a bump in tauri-bundler. + - Bumped due to a bump in cli.rs. + - [2915bd06](https://www.github.com/tauri-apps/tauri/commit/2915bd068ed40dc01a363b69212c6b6f2d3ec01e) fix(bundler): Fix webview install modes in NSIS bundler ([#6854](https://www.github.com/tauri-apps/tauri/pull/6854)) on 2023-05-04 + +## \[1.3.0] + +- Add `--ci` flag and respect the `CI` environment variable on the `signer generate` command. In this case the default password will be an empty string and the CLI will not prompt for a value. + - [8fb1df8a](https://www.github.com/tauri-apps/tauri/commit/8fb1df8aa65a52cdb4a7e1bb9dda9b912a7a2895) feat(cli): add `--ci` flag to `signer generate`, closes [#6089](https://www.github.com/tauri-apps/tauri/pull/6089) ([#6097](https://www.github.com/tauri-apps/tauri/pull/6097)) on 2023-01-19 +- Fix Outdated Github Actions in the Plugin Templates `with-api` and `backend` + - [a926b49a](https://www.github.com/tauri-apps/tauri/commit/a926b49a01925ca757d391994bfac3beea29599b) Fix Github Actions of Tauri Plugin with-api template ([#6603](https://www.github.com/tauri-apps/tauri/pull/6603)) on 2023-04-03 +- Do not crash on Cargo.toml watcher. + - [e8014a7f](https://www.github.com/tauri-apps/tauri/commit/e8014a7f612a1094461ddad63aacc498a2682ff5) fix(cli): do not crash on watcher ([#6303](https://www.github.com/tauri-apps/tauri/pull/6303)) on 2023-02-17 +- Fix crash when nodejs binary has the version in its name, for example `node-18` + - [1c8229fb](https://www.github.com/tauri-apps/tauri/commit/1c8229fbe273554c0c97cccee45d5967f5df1b9f) fix(cli.js): detect `node-` binary, closes [#6427](https://www.github.com/tauri-apps/tauri/pull/6427) ([#6432](https://www.github.com/tauri-apps/tauri/pull/6432)) on 2023-03-16 +- Add `--png` option for the `icon` command to generate custom icon sizes. + - [9d214412](https://www.github.com/tauri-apps/tauri/commit/9d2144128fc5fad67d8404bce95f82297ebb0e4a) feat(cli): add option to make custom icon sizes, closes [#5121](https://www.github.com/tauri-apps/tauri/pull/5121) ([#5246](https://www.github.com/tauri-apps/tauri/pull/5246)) on 2022-12-27 +- Skip the password prompt on the build command when `TAURI_KEY_PASSWORD` environment variable is empty and the `--ci` argument is provided or the `CI` environment variable is set. + - [d4f89af1](https://www.github.com/tauri-apps/tauri/commit/d4f89af18d69fd95a4d8a1ede8442547c6a6d0ee) feat: skip password prompt on the build command if CI is set fixes [#6089](https://www.github.com/tauri-apps/tauri/pull/6089) on 2023-01-18 +- Fix `default-run` not deserialized. + - [57c6bf07](https://www.github.com/tauri-apps/tauri/commit/57c6bf07bb380847abdf27c3fff9891d99c1c98c) fix(cli): fix default-run not deserialized ([#6584](https://www.github.com/tauri-apps/tauri/pull/6584)) on 2023-03-30 +- Fixes HTML serialization removing template tags on the dev server. + - [314f0e21](https://www.github.com/tauri-apps/tauri/commit/314f0e212fd2b9e452bfe3424cdce2b0bf37b5d7) fix(cli): web_dev_server html template serialization (fix [#6165](https://www.github.com/tauri-apps/tauri/pull/6165)) ([#6166](https://www.github.com/tauri-apps/tauri/pull/6166)) on 2023-01-29 +- Use escaping on Handlebars templates. + - [6d6b6e65](https://www.github.com/tauri-apps/tauri/commit/6d6b6e653ea70fc02794f723092cdc860995c259) feat: configure escaping on handlebars templates ([#6678](https://www.github.com/tauri-apps/tauri/pull/6678)) on 2023-05-02 +- Add initial support for building `nsis` bundles on non-Windows platforms. + - [60e6f6c3](https://www.github.com/tauri-apps/tauri/commit/60e6f6c3f1605f3064b5bb177992530ff788ccf0) feat(bundler): Add support for creating NSIS bundles on unix hosts ([#5788](https://www.github.com/tauri-apps/tauri/pull/5788)) on 2023-01-19 +- Add `nsis` bundle target + - [c94e1326](https://www.github.com/tauri-apps/tauri/commit/c94e1326a7c0767a13128a8b1d327a00156ece12) feat(bundler): add `nsis`, closes [#4450](https://www.github.com/tauri-apps/tauri/pull/4450), closes [#2319](https://www.github.com/tauri-apps/tauri/pull/2319) ([#4674](https://www.github.com/tauri-apps/tauri/pull/4674)) on 2023-01-03 +- Remove default features from Cargo.toml template. + - [b08ae637](https://www.github.com/tauri-apps/tauri/commit/b08ae637a0f58b38cbce9b8a1fa0b6c5dc0cfd05) fix(cli): remove default features from template ([#6074](https://www.github.com/tauri-apps/tauri/pull/6074)) on 2023-01-17 +- Use Ubuntu 20.04 to compile the CLI, increasing the minimum libc version required. + +## \[1.2.3] + +- Pin `ignore` to `=0.4.18`. + - [adcb082b](https://www.github.com/tauri-apps/tauri/commit/adcb082b1651ecb2a6208b093e12f4185aa3fc98) chore(deps): pin `ignore` to =0.4.18 on 2023-01-17 + +## \[1.2.2] + +- Detect SvelteKit and Vite for the init and info commands. + - [9d872ab8](https://www.github.com/tauri-apps/tauri/commit/9d872ab8728b1b121909af434adcd5936e5afb7d) feat(cli): detect SvelteKit and Vite ([#5742](https://www.github.com/tauri-apps/tauri/pull/5742)) on 2022-12-02 +- Detect SolidJS and SolidStart for the init and info commands. + - [9e7ce0a8](https://www.github.com/tauri-apps/tauri/commit/9e7ce0a8eef4bf3536645976e3e09162fbf772ab) feat(cli): detect SolidJS and SolidStart ([#5758](https://www.github.com/tauri-apps/tauri/pull/5758)) on 2022-12-08 +- Use older icon types to work around a macOS bug resulting in corrupted 16x16px and 32x32px icons in bundled apps. + - [2d545eff](https://www.github.com/tauri-apps/tauri/commit/2d545eff58734ec70f23f11a429d35435cdf090e) fix(cli): corrupted icons in bundled macOS icons ([#5698](https://www.github.com/tauri-apps/tauri/pull/5698)) on 2022-11-28 + +## \[1.2.1] + +- Fixes injection of Cargo features defined in the configuration file. + - [1ecaeb29](https://www.github.com/tauri-apps/tauri/commit/1ecaeb29aa798f591f6488dc6c3a7a8d22f6073e) fix(cli): inject config feature flags when features arg is not provided on 2022-11-18 + +## \[1.2.0] + +- Detect JSON5 and TOML configuration files in the dev watcher. + - [e7ccbd85](https://www.github.com/tauri-apps/tauri/commit/e7ccbd8573f6b9124e80c0b67fa2365729c3c196) feat(cli): detect JSON5 and TOML configuration files in the dev watcher ([#5439](https://www.github.com/tauri-apps/tauri/pull/5439)) on 2022-10-19 +- Log dev watcher file change detection. + - [9076d5d2](https://www.github.com/tauri-apps/tauri/commit/9076d5d2e76d432aef475ba403e9ab5bd3b9d2b0) feat(cli): add prompt information when file changing detected, closes [#5417](https://www.github.com/tauri-apps/tauri/pull/5417) ([#5428](https://www.github.com/tauri-apps/tauri/pull/5428)) on 2022-10-19 +- Fix crash when nodejs binary has the version in its name, for example `node18` or when running through deno. + - [7a231cd1](https://www.github.com/tauri-apps/tauri/commit/7a231cd1c99101f63354b13bb36223568d2f0a11) fix(cli): detect deno ([#5475](https://www.github.com/tauri-apps/tauri/pull/5475)) on 2022-10-28 +- Changed the project template to not enable all APIs by default. + - [582c25a0](https://www.github.com/tauri-apps/tauri/commit/582c25a0f0fa2725d786ec4edd0defe7811ad6e8) refactor(cli): disable api-all on templates ([#5538](https://www.github.com/tauri-apps/tauri/pull/5538)) on 2022-11-03 + +## \[1.1.1] + +- Fix wrong cli metadata that caused new projects (created through `tauri init`) fail to build + - [db26aaf2](https://www.github.com/tauri-apps/tauri/commit/db26aaf2b44ce5335c9223c571ef2b2175e0cd6d) fix: fix wrong cli metadata ([#5214](https://www.github.com/tauri-apps/tauri/pull/5214)) on 2022-09-16 + +## \[1.1.0] + +- Allow adding `build > beforeBundleCommand` in tauri.conf.json to run a shell command before the bundling phase. + - [57ab9847](https://www.github.com/tauri-apps/tauri/commit/57ab9847eb2d8c9a5da584b873b7c072e9ee26bf) feat(cli): add `beforeBundleCommand`, closes [#4879](https://www.github.com/tauri-apps/tauri/pull/4879) ([#4893](https://www.github.com/tauri-apps/tauri/pull/4893)) on 2022-08-09 +- Change `before_dev_command` and `before_build_command` config value to allow configuring the current working directory. + - [d6f7d3cf](https://www.github.com/tauri-apps/tauri/commit/d6f7d3cfe8a7ec8d68c8341016c4e0a3103da587) Add cwd option to `before` commands, add wait option to dev [#4740](https://www.github.com/tauri-apps/tauri/pull/4740) [#3551](https://www.github.com/tauri-apps/tauri/pull/3551) ([#4834](https://www.github.com/tauri-apps/tauri/pull/4834)) on 2022-08-02 +- Allow configuring the `before_dev_command` to force the CLI to wait for the command to finish before proceeding. + - [d6f7d3cf](https://www.github.com/tauri-apps/tauri/commit/d6f7d3cfe8a7ec8d68c8341016c4e0a3103da587) Add cwd option to `before` commands, add wait option to dev [#4740](https://www.github.com/tauri-apps/tauri/pull/4740) [#3551](https://www.github.com/tauri-apps/tauri/pull/3551) ([#4834](https://www.github.com/tauri-apps/tauri/pull/4834)) on 2022-08-02 +- Check if the default build target is set in the Cargo configuration. + - [436f3d8d](https://www.github.com/tauri-apps/tauri/commit/436f3d8d66727f5b64165522f0b55f4ab54bd1ae) feat(cli): load Cargo configuration to check default build target ([#4990](https://www.github.com/tauri-apps/tauri/pull/4990)) on 2022-08-21 +- Use `cargo metadata` to detect the workspace root and target directory. + - [fea70eff](https://www.github.com/tauri-apps/tauri/commit/fea70effad219c0794d919f8834fa1a1ffd204c7) refactor(cli): Use `cargo metadata` to detect the workspace root and target directory, closes [#4632](https://www.github.com/tauri-apps/tauri/pull/4632), [#4928](https://www.github.com/tauri-apps/tauri/pull/4928). ([#4932](https://www.github.com/tauri-apps/tauri/pull/4932)) on 2022-08-21 +- Prompt for `beforeDevCommand` and `beforeBuildCommand` in `tauri init`. + - [6d4945c9](https://www.github.com/tauri-apps/tauri/commit/6d4945c9f06cd1f7018e1c48686ba682aae817df) feat(cli): prompt for before\*Command, closes [#4691](https://www.github.com/tauri-apps/tauri/pull/4691) ([#4721](https://www.github.com/tauri-apps/tauri/pull/4721)) on 2022-07-25 +- Added support to configuration files in TOML format (Tauri.toml file). + - [ae83d008](https://www.github.com/tauri-apps/tauri/commit/ae83d008f9e1b89bfc8dddaca42aa5c1fbc36f6d) feat: add support to TOML config file `Tauri.toml`, closes [#4806](https://www.github.com/tauri-apps/tauri/pull/4806) ([#4813](https://www.github.com/tauri-apps/tauri/pull/4813)) on 2022-08-02 +- Automatically use any `.taurignore` file as ignore rules for dev watcher and app path finder. + - [596fa08d](https://www.github.com/tauri-apps/tauri/commit/596fa08d48e371c7bd29e1ef799119ac8fca0d0b) feat(cli): automatically use `.taurignore`, ref [#4617](https://www.github.com/tauri-apps/tauri/pull/4617) ([#4623](https://www.github.com/tauri-apps/tauri/pull/4623)) on 2022-07-28 +- Enable WiX FIPS compliance when the `TAURI_FIPS_COMPLIANT` environment variable is set to `true`. + - [d88b9de7](https://www.github.com/tauri-apps/tauri/commit/d88b9de7aaeaaa2e42e4795dbc2b8642b5ae7a50) feat(core): add `fips_compliant` wix config option, closes [#4541](https://www.github.com/tauri-apps/tauri/pull/4541) ([#4843](https://www.github.com/tauri-apps/tauri/pull/4843)) on 2022-08-04 +- Fixes dev watcher incorrectly exiting the CLI when sequential file updates are detected. + - [47fab680](https://www.github.com/tauri-apps/tauri/commit/47fab6809a1e23b3b9a84695e2d91ff0826ba79a) fix(cli): dev watcher incorrectly killing process on multiple file write ([#4684](https://www.github.com/tauri-apps/tauri/pull/4684)) on 2022-07-25 +- Add `libc` field to Node packages. + - [f7d2dfc7](https://www.github.com/tauri-apps/tauri/commit/f7d2dfc7a6d086dd1a218d6c1492b3fef8a64f03) chore: add libc field to node packages ([#4856](https://www.github.com/tauri-apps/tauri/pull/4856)) on 2022-08-04 +- Set the `MACOSX_DEPLOYMENT_TARGET` environment variable with the configuration `minimum_system_version` value. + - [fa23310f](https://www.github.com/tauri-apps/tauri/commit/fa23310f23cb9e6a02ec2524f1ef394a5b42990e) fix(cli): set MACOSX_DEPLOYMENT_TARGET env var, closes [#4704](https://www.github.com/tauri-apps/tauri/pull/4704) ([#4842](https://www.github.com/tauri-apps/tauri/pull/4842)) on 2022-08-02 +- Added `--no-watch` argument to the `dev` command to disable the file watcher. + - [0983d7ce](https://www.github.com/tauri-apps/tauri/commit/0983d7ce7f24ab43f9ae7b5e1177ff244d8885a8) feat(cli): add `--no-watch` argument to the dev command, closes [#4617](https://www.github.com/tauri-apps/tauri/pull/4617) ([#4793](https://www.github.com/tauri-apps/tauri/pull/4793)) on 2022-07-29 +- Validate updater signature matches configured public key. + - [b2a8930b](https://www.github.com/tauri-apps/tauri/commit/b2a8930b3c4b72c50ce72e73575f42c9cbe91bad) feat(cli): validate updater private key when signing ([#4754](https://www.github.com/tauri-apps/tauri/pull/4754)) on 2022-07-25 + +## \[1.0.5] + +- Correctly fill the architecture when building Debian packages targeting ARM64 (aarch64). + - Bumped due to a bump in cli.rs. + - [635f23b8](https://www.github.com/tauri-apps/tauri/commit/635f23b88adbb8726d628f67840709cd870836dc) fix(bundler): correctly set debian architecture for aarch64 ([#4700](https://www.github.com/tauri-apps/tauri/pull/4700)) on 2022-07-17 + +## \[1.0.4] + +- Do not capture and force colors of `cargo build` output. + - [c635a0da](https://www.github.com/tauri-apps/tauri/commit/c635a0dad437860d54109adffaf245b7c21bc684) refactor(cli): do not capture and force colors of cargo build output ([#4627](https://www.github.com/tauri-apps/tauri/pull/4627)) on 2022-07-12 +- Reduce the amount of allocations when converting cases. + - [bc370e32](https://www.github.com/tauri-apps/tauri/commit/bc370e326810446e15b1f50fb962b980114ba16b) feat: reduce the amount of `heck`-related allocations ([#4634](https://www.github.com/tauri-apps/tauri/pull/4634)) on 2022-07-11 + +## \[1.0.3] + +- Changed the app template to not set the default app menu as it is now set automatically on macOS which is the platform that needs a menu to function properly. + - [91055883](https://www.github.com/tauri-apps/tauri/commit/9105588373cc8401bd9ad79bdef26f509b2d76b7) feat: add implicit default menu for macOS only, closes [#4551](https://www.github.com/tauri-apps/tauri/pull/4551) ([#4570](https://www.github.com/tauri-apps/tauri/pull/4570)) on 2022-07-04 +- Improved bundle identifier validation showing the exact source of the configuration value. + - [8e3e7fc6](https://www.github.com/tauri-apps/tauri/commit/8e3e7fc64641afc7a6833bc93205e6f525562545) feat(cli): improve bundle identifier validation, closes [#4589](https://www.github.com/tauri-apps/tauri/pull/4589) ([#4596](https://www.github.com/tauri-apps/tauri/pull/4596)) on 2022-07-05 +- Improve configuration deserialization error messages. + - [9170c920](https://www.github.com/tauri-apps/tauri/commit/9170c9207044fa561535f624916dfdbaa41ff79d) feat(core): improve config deserialization error messages ([#4607](https://www.github.com/tauri-apps/tauri/pull/4607)) on 2022-07-06 +- Revert the `run` command to run in a separate thread. + - [f65eb4f8](https://www.github.com/tauri-apps/tauri/commit/f65eb4f84d8e511cd30d01d20a8223a297f7e584) fix(cli.js): revert `run` command to be nonblocking on 2022-07-04 +- Skip the static link of the `vcruntime140.dll` if the `STATIC_VCRUNTIME` environment variable is set to `false`. + - [2e61abaa](https://www.github.com/tauri-apps/tauri/commit/2e61abaa9ae5d7a41ca1fa6505b5d6c368625ce5) feat(cli): allow dynamic link vcruntime, closes [#4565](https://www.github.com/tauri-apps/tauri/pull/4565) ([#4601](https://www.github.com/tauri-apps/tauri/pull/4601)) on 2022-07-06 +- The `TAURI_CONFIG` environment variable now represents the configuration to be merged instead of the entire JSON. + - [fa028ebf](https://www.github.com/tauri-apps/tauri/commit/fa028ebf3c8ca7b43a70d283a01dbea86217594f) refactor: do not pass entire config from CLI to core, send patch instead ([#4598](https://www.github.com/tauri-apps/tauri/pull/4598)) on 2022-07-06 +- Watch for Cargo workspace members in the `dev` file watcher. + - [dbb8c87b](https://www.github.com/tauri-apps/tauri/commit/dbb8c87b96dec9942b1bf877b29bafb8246514d4) feat(cli): watch Cargo workspaces in the dev command, closes [#4222](https://www.github.com/tauri-apps/tauri/pull/4222) ([#4572](https://www.github.com/tauri-apps/tauri/pull/4572)) on 2022-07-03 + +## \[1.0.2] + +- Fixes a crash on the `signer sign` command. + - [8e808fec](https://www.github.com/tauri-apps/tauri/commit/8e808fece95f2e506acf2c446d37b9913fd67d50) fix(cli.rs): conflicts_with arg doesn't exist closes ([#4538](https://www.github.com/tauri-apps/tauri/pull/4538)) on 2022-06-30 + +## \[1.0.1] + +- No longer adds the `pkg-config` dependency to `.deb` packages when the `systemTray` is used. + This only works with recent versions of `libappindicator-sys` (including https://github.com/tauri-apps/libappindicator-rs/pull/38), + so a `cargo update` may be necessary if you create `.deb` bundles and use the tray feature. + - [0e6edeb1](https://www.github.com/tauri-apps/tauri/commit/0e6edeb14f379af1e02a7cebb4e3a5c9e87ebf7f) fix(cli): Don't add `pkg-config` to `deb` ([#4508](https://www.github.com/tauri-apps/tauri/pull/4508)) on 2022-06-29 +- AppImage bundling will now prefer bundling correctly named appindicator library (including `.1` version suffix). With a symlink for compatibility with the old naming. + - [bf45ca1d](https://www.github.com/tauri-apps/tauri/commit/bf45ca1df6691c05bdf72c5716cc01e89a7791d4) fix(cli,bundler): prefer AppImage libraries with ABI version ([#4505](https://www.github.com/tauri-apps/tauri/pull/4505)) on 2022-06-29 +- Improve error message when `cargo` is not installed. + - [e0e5f772](https://www.github.com/tauri-apps/tauri/commit/e0e5f772430f6349ec99ba891e601331e376e3c7) feat(cli): improve `cargo not found` error message, closes [#4428](https://www.github.com/tauri-apps/tauri/pull/4428) ([#4430](https://www.github.com/tauri-apps/tauri/pull/4430)) on 2022-06-21 +- The app template now only sets the default menu on macOS. + - [5105b428](https://www.github.com/tauri-apps/tauri/commit/5105b428c4726b2179cd4b3244350d1a1ee73734) feat(cli): change app template to only set default menu on macOS ([#4518](https://www.github.com/tauri-apps/tauri/pull/4518)) on 2022-06-29 +- Warn if updater is enabled but not in the bundle target list. + - [31c15cd2](https://www.github.com/tauri-apps/tauri/commit/31c15cd2bd94dbe39fb94982a15cbe02ac5d8925) docs(config): enhance documentation for bundle targets, closes [#3251](https://www.github.com/tauri-apps/tauri/pull/3251) ([#4418](https://www.github.com/tauri-apps/tauri/pull/4418)) on 2022-06-21 +- Check if target exists and is installed on dev and build commands. + - [13b8a240](https://www.github.com/tauri-apps/tauri/commit/13b8a2403d1353a8c3a643fbc6b6e862af68376e) feat(cli): validate target argument ([#4458](https://www.github.com/tauri-apps/tauri/pull/4458)) on 2022-06-24 +- Fixes the covector configuration on the plugin templates. + - [b8a64d01](https://www.github.com/tauri-apps/tauri/commit/b8a64d01bab11f955b7bbdf323d0afa1a3db4b64) fix(cli): add prepublish scripts to the plugin templates on 2022-06-19 +- Set the binary name to the product name in development. + - [b025b9f5](https://www.github.com/tauri-apps/tauri/commit/b025b9f581ac1a6ae0a26789c2be1e9928fb0282) refactor(cli): set binary name on dev ([#4447](https://www.github.com/tauri-apps/tauri/pull/4447)) on 2022-06-23 +- Allow registering a `.gitignore` file to skip watching some project files and directories via the `TAURI_DEV_WATCHER_IGNORE_FILE` environment variable. + - [83186dd8](https://www.github.com/tauri-apps/tauri/commit/83186dd89768407984db35fb67c3cc51f50ea8f5) Read extra ignore file for dev watcher, closes [#4406](https://www.github.com/tauri-apps/tauri/pull/4406) ([#4409](https://www.github.com/tauri-apps/tauri/pull/4409)) on 2022-06-20 +- Fix shebang for `kill-children.sh`. + - [35dd51db](https://www.github.com/tauri-apps/tauri/commit/35dd51db6826ec1eed7b90082b9eb6b2a699b627) fix(cli): add shebang for kill-children.sh, closes [#4262](https://www.github.com/tauri-apps/tauri/pull/4262) ([#4416](https://www.github.com/tauri-apps/tauri/pull/4416)) on 2022-06-22 +- Update plugin templates to use newer `tauri-apps/create-pull-request` GitHub action. + - [07f90795](https://www.github.com/tauri-apps/tauri/commit/07f9079532a42f3517d96faeaf46cad6176b31ac) chore(cli): update plugin template tauri-apps/create-pull-request on 2022-06-19 +- Use UNIX path separator on the init `$schema` field. + - [01053045](https://www.github.com/tauri-apps/tauri/commit/010530459ef62c48eed68ca965f2688accabcf69) chore(cli): use unix path separator on $schema ([#4384](https://www.github.com/tauri-apps/tauri/pull/4384)) on 2022-06-19 +- The `info` command now can check the Cargo lockfile on workspaces. + - [12f65219](https://www.github.com/tauri-apps/tauri/commit/12f65219ea75a51ebd38659ddce1563e015a036c) fix(cli): read lockfile from workspace on the info command, closes [#4232](https://www.github.com/tauri-apps/tauri/pull/4232) ([#4423](https://www.github.com/tauri-apps/tauri/pull/4423)) on 2022-06-21 +- Preserve the `Cargo.toml` formatting when the features array is not changed. + - [6650e5d6](https://www.github.com/tauri-apps/tauri/commit/6650e5d6720c215530ca1fdccd19bd2948dd6ca3) fix(cli): preserve Cargo manifest formatting when possible ([#4431](https://www.github.com/tauri-apps/tauri/pull/4431)) on 2022-06-21 +- Change the updater signature metadata to include the file name instead of its full path. + - [094b3eb3](https://www.github.com/tauri-apps/tauri/commit/094b3eb352bcf5de28414015e7c44290d619ea8c) fix(cli): file name instead of path on updater sig comment, closes [#4467](https://www.github.com/tauri-apps/tauri/pull/4467) ([#4484](https://www.github.com/tauri-apps/tauri/pull/4484)) on 2022-06-27 +- Validate bundle identifier as it must only contain alphanumeric characters, hyphens and periods. + - [0674a801](https://www.github.com/tauri-apps/tauri/commit/0674a80129d7c31bc93257849afc0a5069129fed) fix: assert config.bundle.identifier to be only alphanumeric, hyphens or dots. closes [#4359](https://www.github.com/tauri-apps/tauri/pull/4359) ([#4363](https://www.github.com/tauri-apps/tauri/pull/4363)) on 2022-06-17 + +## \[1.0.0] + +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.16] + +- Use the default window menu in the app template. + - [4c4acc30](https://www.github.com/tauri-apps/tauri/commit/4c4acc3094218dd9cee0f1ad61810c979e0b41fa) feat: implement `Default` for `Menu`, closes [#2398](https://www.github.com/tauri-apps/tauri/pull/2398) ([#4291](https://www.github.com/tauri-apps/tauri/pull/4291)) on 2022-06-15 + +## \[1.0.0-rc.15] + +- Removed the tray icon from the Debian and AppImage bundles since they are embedded in the binary now. + - [4ce8e228](https://www.github.com/tauri-apps/tauri/commit/4ce8e228134cd3f22973b74ef26ca0d165fbbbd9) refactor(core): use `Icon` for tray icons ([#4342](https://www.github.com/tauri-apps/tauri/pull/4342)) on 2022-06-14 + +## \[1.0.0-rc.14] + +- Set the `TRAY_LIBRARY_PATH` environment variable to make the bundle copy the appindicator library to the AppImage. + - [34552444](https://www.github.com/tauri-apps/tauri/commit/3455244436578003a5fbb447b039e5c8971152ec) feat(cli): bundle appindicator library in the AppImage, closes [#3859](https://www.github.com/tauri-apps/tauri/pull/3859) ([#4267](https://www.github.com/tauri-apps/tauri/pull/4267)) on 2022-06-07 +- Set the `APPIMAGE_BUNDLE_GSTREAMER` environment variable to make the bundler copy additional gstreamer files to the AppImage. + - [d335fae9](https://www.github.com/tauri-apps/tauri/commit/d335fae92cdcbb0ee18aad4e54558914afa3e778) feat(bundler): bundle additional gstreamer files, closes [#4092](https://www.github.com/tauri-apps/tauri/pull/4092) ([#4271](https://www.github.com/tauri-apps/tauri/pull/4271)) on 2022-06-10 +- Configure the AppImage bundler to copy the `/usr/bin/xdg-open` binary if it exists and the shell `open` API is enabled. + - [2322ac11](https://www.github.com/tauri-apps/tauri/commit/2322ac11cf6290c6bf65413048a049c8072f863b) fix(bundler): bundle `/usr/bin/xdg-open` in appimage if open API enabled ([#4265](https://www.github.com/tauri-apps/tauri/pull/4265)) on 2022-06-04 +- Fixes multiple occurrences handling of the `bundles` and `features` arguments. + - [f685df39](https://www.github.com/tauri-apps/tauri/commit/f685df399a5a05480b6e4f5d92da71f3b87895ef) fix(cli): parsing of arguments with multiple values, closes [#4231](https://www.github.com/tauri-apps/tauri/pull/4231) ([#4233](https://www.github.com/tauri-apps/tauri/pull/4233)) on 2022-05-29 +- Log command output in real time instead of waiting for it to finish. + - [76d1eaae](https://www.github.com/tauri-apps/tauri/commit/76d1eaaebda5c8f6b0d41bf6587945e98cd441f3) feat(cli): debug command output in real time ([#4318](https://www.github.com/tauri-apps/tauri/pull/4318)) on 2022-06-12 +- Configure the `STATIC_VCRUNTIME` environment variable so `tauri-build` statically links it on the build command. + - [d703d27a](https://www.github.com/tauri-apps/tauri/commit/d703d27a707edc028f13b35603205da1133fcc2b) fix(build): statically link VC runtime only on `tauri build` ([#4292](https://www.github.com/tauri-apps/tauri/pull/4292)) on 2022-06-07 +- Use the `TAURI_TRAY` environment variable to determine which package should be added to the Debian `depends` section. Possible values are `ayatana` and `gtk`. + - [6216eb49](https://www.github.com/tauri-apps/tauri/commit/6216eb49e72863bfb6d4c9edb8827b21406ac393) refactor(core): drop `ayatana-tray` and `gtk-tray` Cargo features ([#4247](https://www.github.com/tauri-apps/tauri/pull/4247)) on 2022-06-02 + +## \[1.0.0-rc.13] + +- Check if `$CWD/src-tauri/tauri.conf.json` exists before walking through the file tree to find the tauri dir in case the whole project is gitignored. + - [bd8f3e29](https://www.github.com/tauri-apps/tauri/commit/bd8f3e298a0cb71809f2e93cc3ebc8e6e5b6a626) fix(cli): manual config lookup to handle gitignored folders, fixes [#3527](https://www.github.com/tauri-apps/tauri/pull/3527) ([#4224](https://www.github.com/tauri-apps/tauri/pull/4224)) on 2022-05-26 +- Statically link the Visual C++ runtime instead of using a merge module on the installer. + - [bb061509](https://www.github.com/tauri-apps/tauri/commit/bb061509fb674bef86ecbc1de3aa8f3e367a9907) refactor(core): statically link vcruntime, closes [#4122](https://www.github.com/tauri-apps/tauri/pull/4122) ([#4227](https://www.github.com/tauri-apps/tauri/pull/4227)) on 2022-05-27 + +## \[1.0.0-rc.12] + +- Properly fetch the NPM dependency information when using Yarn 2+. + - [cdfa6255](https://www.github.com/tauri-apps/tauri/commit/cdfa62551115586725bd3e4c04f12c5256f20790) fix(cli): properly read info when using yarn 2+, closes [#4106](https://www.github.com/tauri-apps/tauri/pull/4106) ([#4193](https://www.github.com/tauri-apps/tauri/pull/4193)) on 2022-05-23 + +## \[1.0.0-rc.11] + +- Allow configuring the display options for the MSI execution allowing quieter updates. + - [9f2c3413](https://www.github.com/tauri-apps/tauri/commit/9f2c34131952ea83c3f8e383bc3cec7e1450429f) feat(core): configure msiexec display options, closes [#3951](https://www.github.com/tauri-apps/tauri/pull/3951) ([#4061](https://www.github.com/tauri-apps/tauri/pull/4061)) on 2022-05-15 + +## \[1.0.0-rc.10] + +- Resolve binary file extension from target triple instead of compile-time checks to allow cross compilation. + - [4562e671](https://www.github.com/tauri-apps/tauri/commit/4562e671e4795e9386429348bf738f7078706945) fix(build): append .exe binary based on target triple instead of running OS, closes [#3870](https://www.github.com/tauri-apps/tauri/pull/3870) ([#4032](https://www.github.com/tauri-apps/tauri/pull/4032)) on 2022-05-03 +- Fixes text overflow on `tauri dev` on Windows. + - [094534d1](https://www.github.com/tauri-apps/tauri/commit/094534d138a9286e4746b61adff2da616e3b6a61) fix(cli): dev command stderr text overflow on Windows, closes [#3995](https://www.github.com/tauri-apps/tauri/pull/3995) ([#4000](https://www.github.com/tauri-apps/tauri/pull/4000)) on 2022-04-29 +- Improve CLI's logging output, making use of the standard rust `log` system. + - [35f21471](https://www.github.com/tauri-apps/tauri/commit/35f2147161e6697cbd2824681eeaf870b5a991c2) feat(cli): Improve CLI logging ([#4060](https://www.github.com/tauri-apps/tauri/pull/4060)) on 2022-05-07 +- Don't override the default keychain on macOS while code signing. + - [a4fcaf1d](https://www.github.com/tauri-apps/tauri/commit/a4fcaf1d04aafc3b4d42186f0fb386797d959a9d) fix: don't override default keychain, closes [#4008](https://www.github.com/tauri-apps/tauri/pull/4008) ([#4053](https://www.github.com/tauri-apps/tauri/pull/4053)) on 2022-05-05 +- - Remove startup delay in `tauri dev` caused by checking for a newer cli version. The check is now done upon process exit. +- Add `TAURI_SKIP_UPDATE_CHECK` env variable to skip checking for a newer CLI version. +- [bbabc8cd](https://www.github.com/tauri-apps/tauri/commit/bbabc8cd1ea2c1f6806610fd2d533c99305d320c) fix(cli.rs): remove startup delay in `tauri dev` ([#3999](https://www.github.com/tauri-apps/tauri/pull/3999)) on 2022-04-29 +- Fix `tauri info` panic when a package isn't installed. + - [4f0f3187](https://www.github.com/tauri-apps/tauri/commit/4f0f3187c9e69262ef28350331b368c831ab930a) fix(cli.rs): fix `tauri info` panic when a package isn't installed, closes [#3985](https://www.github.com/tauri-apps/tauri/pull/3985) ([#3996](https://www.github.com/tauri-apps/tauri/pull/3996)) on 2022-04-29 +- Added `$schema` support to `tauri.conf.json`. + - [715cbde3](https://www.github.com/tauri-apps/tauri/commit/715cbde3842a916c4ebeab2cab348e1774b5c192) feat(config): add `$schema` to `tauri.conf.json`, closes [#3464](https://www.github.com/tauri-apps/tauri/pull/3464) ([#4031](https://www.github.com/tauri-apps/tauri/pull/4031)) on 2022-05-03 +- **Breaking change:** The `dev` command now reads the custom config file from CWD instead of the Tauri folder. + - [a1929c6d](https://www.github.com/tauri-apps/tauri/commit/a1929c6dacccd00af4cdbcc4d29cfb98d8428f55) fix(cli): always read custom config file from CWD, closes [#4067](https://www.github.com/tauri-apps/tauri/pull/4067) ([#4074](https://www.github.com/tauri-apps/tauri/pull/4074)) on 2022-05-07 +- Fixes a Powershell crash when sending SIGINT to the dev command. + - [32048486](https://www.github.com/tauri-apps/tauri/commit/320484866b83ecabb01eb58d158e0fedd9dd08be) fix(cli): powershell crashing on SIGINT, closes [#3997](https://www.github.com/tauri-apps/tauri/pull/3997) ([#4007](https://www.github.com/tauri-apps/tauri/pull/4007)) on 2022-04-29 +- Prevent building when the bundle identifier is the default `com.tauri.dev`. + - [95726ebb](https://www.github.com/tauri-apps/tauri/commit/95726ebb6180d371be44bff9f16ca1eee049006a) feat(cli): prevent default bundle identifier from building, closes [#4041](https://www.github.com/tauri-apps/tauri/pull/4041) ([#4042](https://www.github.com/tauri-apps/tauri/pull/4042)) on 2022-05-04 + +## \[1.0.0-rc.9] + +- Exit CLI when Cargo returns a non-compilation error in `tauri dev`. + - [b5622882](https://www.github.com/tauri-apps/tauri/commit/b5622882cf3748e1e5a90915f415c0cd922aaaf8) fix(cli): exit on non-compilation Cargo errors, closes [#3930](https://www.github.com/tauri-apps/tauri/pull/3930) ([#3942](https://www.github.com/tauri-apps/tauri/pull/3942)) on 2022-04-22 +- Notify CLI update when running `tauri dev`. + - [a649aad7](https://www.github.com/tauri-apps/tauri/commit/a649aad7ad26d4578699370d6e63d80edeca1f97) feat(cli): check and notify about updates on `tauri dev`, closes [#3789](https://www.github.com/tauri-apps/tauri/pull/3789) ([#3960](https://www.github.com/tauri-apps/tauri/pull/3960)) on 2022-04-25 +- Kill the `beforeDevCommand` and app processes if the dev command returns an error. + - [485c9743](https://www.github.com/tauri-apps/tauri/commit/485c97438ac956d86bcf3794ceaa626bef968a4e) fix(cli): kill beforeDevCommand if dev code returns an error ([#3907](https://www.github.com/tauri-apps/tauri/pull/3907)) on 2022-04-19 +- Fix `info` command showing outdated text for latest versions. + - [73a4b74a](https://www.github.com/tauri-apps/tauri/commit/73a4b74aea8544e6fda51c1f6697630b0768072c) fix(cli.rs/info): don't show outdated text for latest versions ([#3829](https://www.github.com/tauri-apps/tauri/pull/3829)) on 2022-04-02 +- **Breaking change:** Enable default Cargo features except `tauri/custom-protocol` on the dev command. + - [f2a30d8b](https://www.github.com/tauri-apps/tauri/commit/f2a30d8bc54fc3ba49e16f69a413eca5f61a9b1f) refactor(core): use ayatana appindicator by default, keep option to use gtk ([#3916](https://www.github.com/tauri-apps/tauri/pull/3916)) on 2022-04-19 +- Kill the `beforeDevCommand` process recursively on Unix. + - [e251e1b0](https://www.github.com/tauri-apps/tauri/commit/e251e1b0991d26ab10aea33cfb228f3e7f0f85b5) fix(cli): kill before dev command recursively on Unix, closes [#2794](https://www.github.com/tauri-apps/tauri/pull/2794) ([#3848](https://www.github.com/tauri-apps/tauri/pull/3848)) on 2022-04-03 + +## \[1.0.0-rc.8] + +- Allows the `tauri.conf.json` file to be git ignored on the path lookup function. + - [cc7c2d77](https://www.github.com/tauri-apps/tauri/commit/cc7c2d77da2e4a39ec2a97b080d41a719e6d0161) feat(cli): allow conf path to be gitignored, closes [#3636](https://www.github.com/tauri-apps/tauri/pull/3636) ([#3683](https://www.github.com/tauri-apps/tauri/pull/3683)) on 2022-03-13 +- Remove `minimumSystemVersion: null` from the application template configuration. + - [c81534eb](https://www.github.com/tauri-apps/tauri/commit/c81534ebd873c358e0346c7949aeb171803149a5) feat(cli): use default macOS minimum system version when it is empty ([#3658](https://www.github.com/tauri-apps/tauri/pull/3658)) on 2022-03-13 +- Improve readability of the `info` subcommand output. + - [49d2f13f](https://www.github.com/tauri-apps/tauri/commit/49d2f13fc07d763d5de9bf4b19d00c901776c11d) feat(cli): colorful cli ([#3635](https://www.github.com/tauri-apps/tauri/pull/3635)) on 2022-03-08 +- Fixes DMG bundling on macOS 12.3. + - [348a1ab5](https://www.github.com/tauri-apps/tauri/commit/348a1ab59d2697478a594016016f1fccbf1ac054) fix(bundler): DMG bundling on macOS 12.3 cannot use bless, closes [#3719](https://www.github.com/tauri-apps/tauri/pull/3719) ([#3721](https://www.github.com/tauri-apps/tauri/pull/3721)) on 2022-03-18 +- Fixes resources bundling on Windows when the path is on the root of the Tauri folder. + - [4c84559e](https://www.github.com/tauri-apps/tauri/commit/4c84559e1f3019e7aa2666b10a1a0bd97bb09d24) fix(cli): root resource bundling on Windows, closes [#3539](https://www.github.com/tauri-apps/tauri/pull/3539) ([#3685](https://www.github.com/tauri-apps/tauri/pull/3685)) on 2022-03-13 + +## \[1.0.0-rc.6] + +- Added `tsp` config option under `tauri > bundle > windows`, which enables Time-Stamp Protocol (RFC 3161) for the timestamping + server under code signing on Windows if set to `true`. + - [bdd5f7c2](https://www.github.com/tauri-apps/tauri/commit/bdd5f7c2f03af4af8b60a9527e55bb18525d989b) fix: add support for Time-Stamping Protocol for Windows codesigning (fix [#3563](https://www.github.com/tauri-apps/tauri/pull/3563)) ([#3570](https://www.github.com/tauri-apps/tauri/pull/3570)) on 2022-03-07 +- Added `i686-pc-windows-msvc` to the prebuilt targets. + - [fb6744da](https://www.github.com/tauri-apps/tauri/commit/fb6744daa45165c7e00e5c01f7df0880d69ca509) feat(cli.js): add 32bit cli for windows ([#3540](https://www.github.com/tauri-apps/tauri/pull/3540)) on 2022-02-24 +- Change the `plugin init` templates to use the new `tauri::plugin::Builder` syntax. + - [f7acb061](https://www.github.com/tauri-apps/tauri/commit/f7acb061e4d1ecdbfe182793587632d7ba6d8eff) feat(cli): use plugin::Builder syntax on the plugin template ([#3606](https://www.github.com/tauri-apps/tauri/pull/3606)) on 2022-03-03 + +## \[1.0.0-rc.5] + +- Improve "waiting for your dev server to start" message. + - [5999379f](https://www.github.com/tauri-apps/tauri/commit/5999379fb06052a115f04f99274ab46d1eefd659) chore(cli): improve "waiting for dev server" message, closes [#3491](https://www.github.com/tauri-apps/tauri/pull/3491) ([#3504](https://www.github.com/tauri-apps/tauri/pull/3504)) on 2022-02-18 +- Do not panic if the updater private key password is wrong. + - [17f17a80](https://www.github.com/tauri-apps/tauri/commit/17f17a80f818bcc20c387583a6ff00a8e07ec533) fix(cli): do not panic if private key password is wrong, closes [#3449](https://www.github.com/tauri-apps/tauri/pull/3449) ([#3495](https://www.github.com/tauri-apps/tauri/pull/3495)) on 2022-02-17 +- Check the current folder before checking the directories on the app and tauri dir path lookup function. + - [a06de376](https://www.github.com/tauri-apps/tauri/commit/a06de3760184caa71acfe7a2fe2189a033b565f5) fix(cli): path lookup should not check subfolder before the current one ([#3465](https://www.github.com/tauri-apps/tauri/pull/3465)) on 2022-02-15 +- Fixes the signature of the `signer sign` command to not have duplicated short flags. + - [a9755514](https://www.github.com/tauri-apps/tauri/commit/a975551461f3698db3f3b6afa5101189aaeeada9) fix(cli): duplicated short flag for `signer sign`, closes [#3483](https://www.github.com/tauri-apps/tauri/pull/3483) ([#3492](https://www.github.com/tauri-apps/tauri/pull/3492)) on 2022-02-17 + +## \[1.0.0-rc.4] + +- Change the `run` function to take a callback and run asynchronously instead of blocking the event loop. + - [cd9a20b9](https://www.github.com/tauri-apps/tauri/commit/cd9a20b9ab013759b4bdb742f358988022450795) refactor(cli.js): run on separate thread ([#3436](https://www.github.com/tauri-apps/tauri/pull/3436)) on 2022-02-13 +- Improve error message when the dev runner command fails. + - [759d1afb](https://www.github.com/tauri-apps/tauri/commit/759d1afb86f3657f6071a2ae39c9be21e20ed22c) feat(cli): improve error message when dev runner command fails ([#3447](https://www.github.com/tauri-apps/tauri/pull/3447)) on 2022-02-13 +- Show full error message from `cli.rs` instead of just the outermost underlying error message. + - [63826010](https://www.github.com/tauri-apps/tauri/commit/63826010d1f38544f36afd3aac67c45d4608d15b) feat(cli.js): show full error message ([#3442](https://www.github.com/tauri-apps/tauri/pull/3442)) on 2022-02-13 +- Increase `tauri.conf.json` directory lookup depth to `3` and allow changing it with the `TAURI_PATH_DEPTH` environment variable. + - [c6031c70](https://www.github.com/tauri-apps/tauri/commit/c6031c7070c6bb7539bbfdfe42cb73012829c910) feat(cli): increase lookup depth, add env var option ([#3451](https://www.github.com/tauri-apps/tauri/pull/3451)) on 2022-02-13 +- Added `tauri-build`, `tao` and `wry` version to the `info` command output. + - [16f1173f](https://www.github.com/tauri-apps/tauri/commit/16f1173f456b1db543d0160df2c9828708bfc68a) feat(cli): add tao and wry version to the `info` output ([#3443](https://www.github.com/tauri-apps/tauri/pull/3443)) on 2022-02-13 + +## \[1.0.0-rc.3] + +- Change default value for the `freezePrototype` configuration to `false`. + - Bumped due to a bump in cli.rs. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[1.0.0-rc.2] + +- Fixes Tauri path resolution on projects without Git or a `.gitignore` file. + - [d8acbe11](https://www.github.com/tauri-apps/tauri/commit/d8acbe11492bd990e6983c7e63e0f1a8f1ea5c7c) fix(cli.rs): app path resolution on projects without git, closes [#3409](https://www.github.com/tauri-apps/tauri/pull/3409) ([#3410](https://www.github.com/tauri-apps/tauri/pull/3410)) on 2022-02-11 + +## \[1.0.0-rc.1] + +- Fix `init` command prompting for values even if the argument has been provided on the command line. + - [def76840](https://www.github.com/tauri-apps/tauri/commit/def76840257a1447723ecda13c807cf0c881f083) fix(cli.rs): do not prompt for `init` values if arg set ([#3400](https://www.github.com/tauri-apps/tauri/pull/3400)) on 2022-02-11 + - [41052dee](https://www.github.com/tauri-apps/tauri/commit/41052deeda2a00ee2b8ec2041c9c87c11de82ab2) fix(covector): add cli.js to change files on 2022-02-11 +- Fixes CLI freezing when running `light.exe` on Windows without the `--verbose` flag. + - [8beab636](https://www.github.com/tauri-apps/tauri/commit/8beab6363491e2a8757cc9fc0fa1eccc98ece916) fix(cli): build freezing on Windows, closes [#3399](https://www.github.com/tauri-apps/tauri/pull/3399) ([#3402](https://www.github.com/tauri-apps/tauri/pull/3402)) on 2022-02-11 +- Respect `.gitignore` configuration when looking for the folder with the `tauri.conf.json` file. + - [9c6c5a8c](https://www.github.com/tauri-apps/tauri/commit/9c6c5a8c52c6460d0b0a1a55300e1828262994ba) perf(cli.rs): improve performance of tauri dir lookup reading .gitignore ([#3405](https://www.github.com/tauri-apps/tauri/pull/3405)) on 2022-02-11 + - [41052dee](https://www.github.com/tauri-apps/tauri/commit/41052deeda2a00ee2b8ec2041c9c87c11de82ab2) fix(covector): add cli.js to change files on 2022-02-11 + +## \[1.0.0-rc.0] + +- Do not force Tauri application code on `src-tauri` folder and use a glob pattern to look for a subfolder with a `tauri.conf.json` file. + - [a8cff6b3](https://www.github.com/tauri-apps/tauri/commit/a8cff6b3bc3288a53d7cdc5b3cb95d371309d2d6) feat(cli): do not enforce `src-tauri` folder structure, closes [#2643](https://www.github.com/tauri-apps/tauri/pull/2643) ([#2654](https://www.github.com/tauri-apps/tauri/pull/2654)) on 2021-09-27 +- Added CommonJS output to the `dist` folder. + - [205b0dc8](https://www.github.com/tauri-apps/tauri/commit/205b0dc8f30bf70902979a2c0a08c8bc8c8e5360) feat(cli.js): add CommonJS dist files ([#2646](https://www.github.com/tauri-apps/tauri/pull/2646)) on 2021-09-23 +- Fixes `.ico` icon generation. + - [11db96e4](https://www.github.com/tauri-apps/tauri/commit/11db96e440e6cadc1c70992d07bfea3c448208b1) fix(cli.js): `.ico` icon generation, closes [#2692](https://www.github.com/tauri-apps/tauri/pull/2692) ([#2694](https://www.github.com/tauri-apps/tauri/pull/2694)) on 2021-10-02 +- Automatically unplug `@tauri-apps/cli` in yarn 2+ installations to fix the download of the rust-cli. + - [1e336b68](https://www.github.com/tauri-apps/tauri/commit/1e336b6872c3b78caf7c2c6e71e03016c6abdacf) fix(cli.js): Fix package installation on yarn 2+ ([#3012](https://www.github.com/tauri-apps/tauri/pull/3012)) on 2021-12-09 +- Read `package.json` and check for a `tauri` object containing the `appPath` string, which points to the tauri crate path. + - [fb2b9a52](https://www.github.com/tauri-apps/tauri/commit/fb2b9a52f594830c0a68ea40ea429a09892f7ba7) feat(cli.js): allow configuring tauri app path on package.json [#2752](https://www.github.com/tauri-apps/tauri/pull/2752) ([#3035](https://www.github.com/tauri-apps/tauri/pull/3035)) on 2021-12-09 +- Removed the `icon` command, now exposed as a separate package, see https://github.com/tauri-apps/tauricon. + - [58030172](https://www.github.com/tauri-apps/tauri/commit/58030172eddb2403a84b56a21b5bdcebca42c265) feat(tauricon): remove from cli ([#3293](https://www.github.com/tauri-apps/tauri/pull/3293)) on 2022-02-07 diff --git a/node_modules/@tauri-apps/cli/Cargo.toml b/node_modules/@tauri-apps/cli/Cargo.toml new file mode 100644 index 0000000..2af60d5 --- /dev/null +++ b/node_modules/@tauri-apps/cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +edition = "2021" +name = "tauri-cli-node" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +napi = { version = "2.14", default-features = false, features = ["napi4"] } +napi-derive = "2.14" +tauri-cli = { path = "..", default-features = false } +log = "0.4.20" + +[build-dependencies] +napi-build = "2.1" + +[features] +default = ["tauri-cli/default"] +native-tls = ["tauri-cli/native-tls"] +native-tls-vendored = ["tauri-cli/native-tls-vendored"] diff --git a/node_modules/@tauri-apps/cli/LICENSE_APACHE-2.0 b/node_modules/@tauri-apps/cli/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/node_modules/@tauri-apps/cli/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/node_modules/@tauri-apps/cli/LICENSE_MIT b/node_modules/@tauri-apps/cli/LICENSE_MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/node_modules/@tauri-apps/cli/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/@tauri-apps/cli/README.md b/node_modules/@tauri-apps/cli/README.md new file mode 100644 index 0000000..7cf8ea0 --- /dev/null +++ b/node_modules/@tauri-apps/cli/README.md @@ -0,0 +1,43 @@ +# @tauri-apps/cli + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![test cli](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-cli-js.yml?label=test%20cli&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-cli-js.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------- | ------------------------------------------- | +| @tauri-apps/cli | ![](https://img.shields.io/npm/v/@tauri-apps/cli.svg) | + +## About Tauri +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. +## This module +Written in Typescript and packaged such that it can be used with `npm`, `pnpm`, `yarn`, and `bun`, this library provides a node.js runner for common tasks when using Tauri, like `yarn tauri dev`. For the most part it is a wrapper around [tauri-cli](https://github.com/tauri-apps/tauri/blob/dev/tooling/cli). + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + + +## Installation + +The preferred method is to install this module locally as a development dependency: +``` +$ npm install --save-dev @tauri-apps/cli +$ yarn add --dev @tauri-apps/cli +``` + +## Semver +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). +## Licenses +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/node_modules/@tauri-apps/cli/build.rs b/node_modules/@tauri-apps/cli/build.rs new file mode 100644 index 0000000..3031fe5 --- /dev/null +++ b/node_modules/@tauri-apps/cli/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + ::napi_build::setup(); +} diff --git a/node_modules/@tauri-apps/cli/index.d.ts b/node_modules/@tauri-apps/cli/index.d.ts new file mode 100644 index 0000000..b562ef7 --- /dev/null +++ b/node_modules/@tauri-apps/cli/index.d.ts @@ -0,0 +1,7 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export function run(args: Array, binName: string | undefined | null, callback: (...args: any[]) => any): void +export function logError(error: string): void diff --git a/node_modules/@tauri-apps/cli/index.js b/node_modules/@tauri-apps/cli/index.js new file mode 100644 index 0000000..4bf34ea --- /dev/null +++ b/node_modules/@tauri-apps/cli/index.js @@ -0,0 +1,258 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'cli.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./cli.android-arm64.node') + } else { + nativeBinding = require('@tauri-apps/cli-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'cli.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./cli.android-arm-eabi.node') + } else { + nativeBinding = require('@tauri-apps/cli-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'cli.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.win32-x64-msvc.node') + } else { + nativeBinding = require('@tauri-apps/cli-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'cli.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.win32-ia32-msvc.node') + } else { + nativeBinding = require('@tauri-apps/cli-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'cli.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.win32-arm64-msvc.node') + } else { + nativeBinding = require('@tauri-apps/cli-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'cli.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./cli.darwin-universal.node') + } else { + nativeBinding = require('@tauri-apps/cli-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'cli.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./cli.darwin-x64.node') + } else { + nativeBinding = require('@tauri-apps/cli-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'cli.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.darwin-arm64.node') + } else { + nativeBinding = require('@tauri-apps/cli-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'cli.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./cli.freebsd-x64.node') + } else { + nativeBinding = require('@tauri-apps/cli-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'cli.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.linux-x64-musl.node') + } else { + nativeBinding = require('@tauri-apps/cli-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'cli.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.linux-x64-gnu.node') + } else { + nativeBinding = require('@tauri-apps/cli-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'cli.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.linux-arm64-musl.node') + } else { + nativeBinding = require('@tauri-apps/cli-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'cli.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.linux-arm64-gnu.node') + } else { + nativeBinding = require('@tauri-apps/cli-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + localFileExisted = existsSync( + join(__dirname, 'cli.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./cli.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@tauri-apps/cli-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { run, logError } = nativeBinding + +module.exports.run = run +module.exports.logError = logError diff --git a/node_modules/@tauri-apps/cli/jest.config.js b/node_modules/@tauri-apps/cli/jest.config.js new file mode 100644 index 0000000..c06e63c --- /dev/null +++ b/node_modules/@tauri-apps/cli/jest.config.js @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +module.exports = { + setupFilesAfterEnv: ['/test/jest/jest.setup.js'], + testMatch: [ + '/test/jest/__tests__/**/*.spec.js', + '/test/jest/__tests__/**/*.test.js' + ], + moduleFileExtensions: ['ts', 'js', 'json'], + moduleNameMapper: { + '^~/(.*)$': '/$1' + }, + transform: { + '\\.toml$': 'jest-transform-toml' + } +} diff --git a/node_modules/@tauri-apps/cli/main.d.ts b/node_modules/@tauri-apps/cli/main.d.ts new file mode 100644 index 0000000..a89dae5 --- /dev/null +++ b/node_modules/@tauri-apps/cli/main.d.ts @@ -0,0 +1,8 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* tslint:disable */ +/* eslint-disable */ + +export function run(args: Array, binName: string | undefined | null): Promise diff --git a/node_modules/@tauri-apps/cli/main.js b/node_modules/@tauri-apps/cli/main.js new file mode 100644 index 0000000..b3156ed --- /dev/null +++ b/node_modules/@tauri-apps/cli/main.js @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { run, logError } = require('./index') + +module.exports.run = (args, binName) => { + return new Promise((resolve, reject) => { + run(args, binName, res => { + if (res instanceof Error) { + reject(res) + } else { + resolve(res) + } + }) + }) +} + +module.exports.logError = logError diff --git a/node_modules/@tauri-apps/cli/schema.json b/node_modules/@tauri-apps/cli/schema.json new file mode 100644 index 0000000..2626e04 --- /dev/null +++ b/node_modules/@tauri-apps/cli/schema.json @@ -0,0 +1,3478 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, and `tauri.macos.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```", + "type": "object", + "properties": { + "$schema": { + "description": "The JSON schema for the Tauri config.", + "type": [ + "string", + "null" + ] + }, + "package": { + "description": "Package settings.", + "default": { + "productName": null, + "version": null + }, + "allOf": [ + { + "$ref": "#/definitions/PackageConfig" + } + ] + }, + "tauri": { + "description": "The Tauri configuration.", + "default": { + "allowlist": { + "all": false, + "app": { + "all": false, + "hide": false, + "show": false + }, + "clipboard": { + "all": false, + "readText": false, + "writeText": false + }, + "dialog": { + "all": false, + "ask": false, + "confirm": false, + "message": false, + "open": false, + "save": false + }, + "fs": { + "all": false, + "copyFile": false, + "createDir": false, + "exists": false, + "readDir": false, + "readFile": false, + "removeDir": false, + "removeFile": false, + "renameFile": false, + "scope": [], + "writeFile": false + }, + "globalShortcut": { + "all": false + }, + "http": { + "all": false, + "request": false, + "scope": [] + }, + "notification": { + "all": false + }, + "os": { + "all": false + }, + "path": { + "all": false + }, + "process": { + "all": false, + "exit": false, + "relaunch": false, + "relaunchDangerousAllowSymlinkMacos": false + }, + "protocol": { + "all": false, + "asset": false, + "assetScope": [] + }, + "shell": { + "all": false, + "execute": false, + "open": false, + "scope": [], + "sidecar": false + }, + "window": { + "all": false, + "center": false, + "close": false, + "create": false, + "hide": false, + "maximize": false, + "minimize": false, + "print": false, + "requestUserAttention": false, + "setAlwaysOnTop": false, + "setClosable": false, + "setContentProtected": false, + "setCursorGrab": false, + "setCursorIcon": false, + "setCursorPosition": false, + "setCursorVisible": false, + "setDecorations": false, + "setFocus": false, + "setFullscreen": false, + "setIcon": false, + "setIgnoreCursorEvents": false, + "setMaxSize": false, + "setMaximizable": false, + "setMinSize": false, + "setMinimizable": false, + "setPosition": false, + "setResizable": false, + "setSize": false, + "setSkipTaskbar": false, + "setTitle": false, + "show": false, + "startDragging": false, + "unmaximize": false, + "unminimize": false + } + }, + "bundle": { + "active": false, + "appimage": { + "bundleMediaFramework": false + }, + "deb": { + "files": {} + }, + "dmg": { + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + }, + "windowSize": { + "height": 400, + "width": 660 + } + }, + "icon": [], + "identifier": "", + "macOS": { + "minimumSystemVersion": "10.13" + }, + "rpm": { + "epoch": 0, + "files": {}, + "release": "1" + }, + "targets": "all", + "useLocalToolsDir": false, + "windows": { + "allowDowngrades": true, + "certificateThumbprint": null, + "digestAlgorithm": null, + "nsis": null, + "signCommand": null, + "timestampUrl": null, + "tsp": false, + "webviewFixedRuntimePath": null, + "webviewInstallMode": { + "silent": true, + "type": "downloadBootstrapper" + }, + "wix": null + } + }, + "macOSPrivateApi": false, + "pattern": { + "use": "brownfield" + }, + "security": { + "dangerousDisableAssetCspModification": false, + "dangerousRemoteDomainIpcAccess": [], + "dangerousUseHttpScheme": false, + "freezePrototype": false + }, + "updater": { + "active": false, + "dialog": true, + "pubkey": "", + "windows": { + "installMode": "passive", + "installerArgs": [] + } + }, + "windows": [] + }, + "allOf": [ + { + "$ref": "#/definitions/TauriConfig" + } + ] + }, + "build": { + "description": "The build configuration.", + "default": { + "devPath": "http://localhost:8080/", + "distDir": "../dist", + "withGlobalTauri": false + }, + "allOf": [ + { + "$ref": "#/definitions/BuildConfig" + } + ] + }, + "plugins": { + "description": "The plugins config.", + "default": {}, + "allOf": [ + { + "$ref": "#/definitions/PluginConfig" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "PackageConfig": { + "description": "The package configuration.\n\nSee more: https://tauri.app/v1/api/config#packageconfig", + "type": "object", + "properties": { + "productName": { + "description": "App name.", + "type": [ + "string", + "null" + ], + "pattern": "^[^/\\:*?\"<>|]+$" + }, + "version": { + "description": "App version. It is a semver version number or a path to a `package.json` file containing the `version` field. If removed the version number from `Cargo.toml` is used.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "TauriConfig": { + "description": "The Tauri configuration object.\n\nSee more: https://tauri.app/v1/api/config#tauriconfig", + "type": "object", + "properties": { + "pattern": { + "description": "The pattern to use.", + "default": { + "use": "brownfield" + }, + "allOf": [ + { + "$ref": "#/definitions/PatternKind" + } + ] + }, + "windows": { + "description": "The windows configuration.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/WindowConfig" + } + }, + "cli": { + "description": "The CLI configuration.", + "anyOf": [ + { + "$ref": "#/definitions/CliConfig" + }, + { + "type": "null" + } + ] + }, + "bundle": { + "description": "The bundler configuration.", + "default": { + "active": false, + "appimage": { + "bundleMediaFramework": false + }, + "deb": { + "files": {} + }, + "dmg": { + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + }, + "windowSize": { + "height": 400, + "width": 660 + } + }, + "icon": [], + "identifier": "", + "macOS": { + "minimumSystemVersion": "10.13" + }, + "rpm": { + "epoch": 0, + "files": {}, + "release": "1" + }, + "targets": "all", + "useLocalToolsDir": false, + "windows": { + "allowDowngrades": true, + "certificateThumbprint": null, + "digestAlgorithm": null, + "nsis": null, + "signCommand": null, + "timestampUrl": null, + "tsp": false, + "webviewFixedRuntimePath": null, + "webviewInstallMode": { + "silent": true, + "type": "downloadBootstrapper" + }, + "wix": null + } + }, + "allOf": [ + { + "$ref": "#/definitions/BundleConfig" + } + ] + }, + "allowlist": { + "description": "The allowlist configuration.", + "default": { + "all": false, + "app": { + "all": false, + "hide": false, + "show": false + }, + "clipboard": { + "all": false, + "readText": false, + "writeText": false + }, + "dialog": { + "all": false, + "ask": false, + "confirm": false, + "message": false, + "open": false, + "save": false + }, + "fs": { + "all": false, + "copyFile": false, + "createDir": false, + "exists": false, + "readDir": false, + "readFile": false, + "removeDir": false, + "removeFile": false, + "renameFile": false, + "scope": [], + "writeFile": false + }, + "globalShortcut": { + "all": false + }, + "http": { + "all": false, + "request": false, + "scope": [] + }, + "notification": { + "all": false + }, + "os": { + "all": false + }, + "path": { + "all": false + }, + "process": { + "all": false, + "exit": false, + "relaunch": false, + "relaunchDangerousAllowSymlinkMacos": false + }, + "protocol": { + "all": false, + "asset": false, + "assetScope": [] + }, + "shell": { + "all": false, + "execute": false, + "open": false, + "scope": [], + "sidecar": false + }, + "window": { + "all": false, + "center": false, + "close": false, + "create": false, + "hide": false, + "maximize": false, + "minimize": false, + "print": false, + "requestUserAttention": false, + "setAlwaysOnTop": false, + "setClosable": false, + "setContentProtected": false, + "setCursorGrab": false, + "setCursorIcon": false, + "setCursorPosition": false, + "setCursorVisible": false, + "setDecorations": false, + "setFocus": false, + "setFullscreen": false, + "setIcon": false, + "setIgnoreCursorEvents": false, + "setMaxSize": false, + "setMaximizable": false, + "setMinSize": false, + "setMinimizable": false, + "setPosition": false, + "setResizable": false, + "setSize": false, + "setSkipTaskbar": false, + "setTitle": false, + "show": false, + "startDragging": false, + "unmaximize": false, + "unminimize": false + } + }, + "allOf": [ + { + "$ref": "#/definitions/AllowlistConfig" + } + ] + }, + "security": { + "description": "Security configuration.", + "default": { + "dangerousDisableAssetCspModification": false, + "dangerousRemoteDomainIpcAccess": [], + "dangerousUseHttpScheme": false, + "freezePrototype": false + }, + "allOf": [ + { + "$ref": "#/definitions/SecurityConfig" + } + ] + }, + "updater": { + "description": "The updater configuration.", + "default": { + "active": false, + "dialog": true, + "pubkey": "", + "windows": { + "installMode": "passive", + "installerArgs": [] + } + }, + "allOf": [ + { + "$ref": "#/definitions/UpdaterConfig" + } + ] + }, + "systemTray": { + "description": "Configuration for app system tray.", + "anyOf": [ + { + "$ref": "#/definitions/SystemTrayConfig" + }, + { + "type": "null" + } + ] + }, + "macOSPrivateApi": { + "description": "MacOS private API configuration. Enables the transparent background API and sets the `fullScreenEnabled` preference to `true`.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PatternKind": { + "description": "The application pattern.", + "oneOf": [ + { + "description": "Brownfield pattern.", + "type": "object", + "required": [ + "use" + ], + "properties": { + "use": { + "type": "string", + "enum": [ + "brownfield" + ] + } + } + }, + { + "description": "Isolation pattern. Recommended for security purposes.", + "type": "object", + "required": [ + "options", + "use" + ], + "properties": { + "use": { + "type": "string", + "enum": [ + "isolation" + ] + }, + "options": { + "type": "object", + "required": [ + "dir" + ], + "properties": { + "dir": { + "description": "The dir containing the index.html file that contains the secure isolation application.", + "type": "string" + } + } + } + } + } + ] + }, + "WindowConfig": { + "description": "The window configuration object.\n\nSee more: https://tauri.app/v1/api/config#windowconfig", + "type": "object", + "properties": { + "label": { + "description": "The window identifier. It must be alphanumeric.", + "default": "main", + "type": "string" + }, + "url": { + "description": "The window webview URL.", + "default": "index.html", + "allOf": [ + { + "$ref": "#/definitions/WindowUrl" + } + ] + }, + "userAgent": { + "description": "The user agent for the webview", + "type": [ + "string", + "null" + ] + }, + "fileDropEnabled": { + "description": "Whether the file drop is enabled or not on the webview. By default it is enabled.\n\nDisabling it is required to use drag and drop on the frontend on Windows.", + "default": true, + "type": "boolean" + }, + "center": { + "description": "Whether or not the window starts centered or not.", + "default": false, + "type": "boolean" + }, + "x": { + "description": "The horizontal position of the window's top left corner", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "y": { + "description": "The vertical position of the window's top left corner", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "width": { + "description": "The window width.", + "default": 800.0, + "type": "number", + "format": "double" + }, + "height": { + "description": "The window height.", + "default": 600.0, + "type": "number", + "format": "double" + }, + "minWidth": { + "description": "The min window width.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "minHeight": { + "description": "The min window height.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "maxWidth": { + "description": "The max window width.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "maxHeight": { + "description": "The max window height.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "resizable": { + "description": "Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.", + "default": true, + "type": "boolean" + }, + "maximizable": { + "description": "Whether the window's native maximize button is enabled or not. If resizable is set to false, this setting is ignored.\n\n## Platform-specific\n\n- **macOS:** Disables the \"zoom\" button in the window titlebar, which is also used to enter fullscreen mode. - **Linux / iOS / Android:** Unsupported.", + "default": true, + "type": "boolean" + }, + "minimizable": { + "description": "Whether the window's native minimize button is enabled or not.\n\n## Platform-specific\n\n- **Linux / iOS / Android:** Unsupported.", + "default": true, + "type": "boolean" + }, + "closable": { + "description": "Whether the window's native close button is enabled or not.\n\n## Platform-specific\n\n- **Linux:** \"GTK+ will do its best to convince the window manager not to show a close button. Depending on the system, this function may not have any effect when called on a window that is already visible\" - **iOS / Android:** Unsupported.", + "default": true, + "type": "boolean" + }, + "title": { + "description": "The window title.", + "default": "Tauri App", + "type": "string" + }, + "fullscreen": { + "description": "Whether the window starts as fullscreen or not.", + "default": false, + "type": "boolean" + }, + "focus": { + "description": "Whether the window will be initially focused or not.", + "default": true, + "type": "boolean" + }, + "transparent": { + "description": "Whether the window is transparent or not.\n\nNote that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`. WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.", + "default": false, + "type": "boolean" + }, + "maximized": { + "description": "Whether the window is maximized or not.", + "default": false, + "type": "boolean" + }, + "visible": { + "description": "Whether the window is visible or not.", + "default": true, + "type": "boolean" + }, + "decorations": { + "description": "Whether the window should have borders and bars.", + "default": true, + "type": "boolean" + }, + "alwaysOnTop": { + "description": "Whether the window should always be on top of other windows.", + "default": false, + "type": "boolean" + }, + "contentProtected": { + "description": "Prevents the window contents from being captured by other apps.", + "default": false, + "type": "boolean" + }, + "skipTaskbar": { + "description": "If `true`, hides the window icon from the taskbar on Windows and Linux.", + "default": false, + "type": "boolean" + }, + "theme": { + "description": "The initial window theme. Defaults to the system theme. Only implemented on Windows and macOS 10.14+.", + "anyOf": [ + { + "$ref": "#/definitions/Theme" + }, + { + "type": "null" + } + ] + }, + "titleBarStyle": { + "description": "The style of the macOS title bar.", + "default": "Visible", + "allOf": [ + { + "$ref": "#/definitions/TitleBarStyle" + } + ] + }, + "hiddenTitle": { + "description": "If `true`, sets the window title to be hidden on macOS.", + "default": false, + "type": "boolean" + }, + "acceptFirstMouse": { + "description": "Whether clicking an inactive window also clicks through to the webview on macOS.", + "default": false, + "type": "boolean" + }, + "tabbingIdentifier": { + "description": "Defines the window [tabbing identifier] for macOS.\n\nWindows with matching tabbing identifiers will be grouped together. If the tabbing identifier is not set, automatic tabbing will be disabled.\n\n[tabbing identifier]: ", + "type": [ + "string", + "null" + ] + }, + "additionalBrowserArgs": { + "description": "Defines additional browser arguments on Windows. By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` so if you use this method, you also need to disable these components by yourself if you want.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "WindowUrl": { + "description": "An URL to open on a Tauri webview window.", + "anyOf": [ + { + "description": "An external URL.", + "type": "string", + "format": "uri" + }, + { + "description": "The path portion of an app URL. For instance, to load `tauri://localhost/users/john`, you can simply provide `users/john` in this configuration.", + "type": "string" + } + ] + }, + "Theme": { + "description": "System theme.", + "oneOf": [ + { + "description": "Light theme.", + "type": "string", + "enum": [ + "Light" + ] + }, + { + "description": "Dark theme.", + "type": "string", + "enum": [ + "Dark" + ] + } + ] + }, + "TitleBarStyle": { + "description": "How the window title bar should be displayed on macOS.", + "oneOf": [ + { + "description": "A normal title bar.", + "type": "string", + "enum": [ + "Visible" + ] + }, + { + "description": "Makes the title bar transparent, so the window background color is shown instead.\n\nUseful if you don't need to have actual HTML under the title bar. This lets you avoid the caveats of using `TitleBarStyle::Overlay`. Will be more useful when Tauri lets you set a custom window background color.", + "type": "string", + "enum": [ + "Transparent" + ] + }, + { + "description": "Shows the title bar as a transparent overlay over the window's content.\n\nKeep in mind: - The height of the title bar is different on different OS versions, which can lead to window the controls and title not being where you don't expect. - You need to define a custom drag region to make your window draggable, however due to a limitation you can't drag the window when it's not in focus . - The color of the window title depends on the system theme.", + "type": "string", + "enum": [ + "Overlay" + ] + } + ] + }, + "CliConfig": { + "description": "describes a CLI configuration\n\nSee more: https://tauri.app/v1/api/config#cliconfig", + "type": "object", + "properties": { + "description": { + "description": "Command description which will be shown on the help information.", + "type": [ + "string", + "null" + ] + }, + "longDescription": { + "description": "Command long description which will be shown on the help information.", + "type": [ + "string", + "null" + ] + }, + "beforeHelp": { + "description": "Adds additional help information to be displayed in addition to auto-generated help. This information is displayed before the auto-generated help information. This is often used for header information.", + "type": [ + "string", + "null" + ] + }, + "afterHelp": { + "description": "Adds additional help information to be displayed in addition to auto-generated help. This information is displayed after the auto-generated help information. This is often used to describe how to use the arguments, or caveats to be noted.", + "type": [ + "string", + "null" + ] + }, + "args": { + "description": "List of arguments for the command", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/CliArg" + } + }, + "subcommands": { + "description": "List of subcommands of this command", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/CliConfig" + } + } + }, + "additionalProperties": false + }, + "CliArg": { + "description": "A CLI argument definition.", + "type": "object", + "required": [ + "name" + ], + "properties": { + "short": { + "description": "The short version of the argument, without the preceding -.\n\nNOTE: Any leading `-` characters will be stripped, and only the first non-character will be used as the short version.", + "type": [ + "string", + "null" + ], + "maxLength": 1, + "minLength": 1 + }, + "name": { + "description": "The unique argument name", + "type": "string" + }, + "description": { + "description": "The argument description which will be shown on the help information. Typically, this is a short (one line) description of the arg.", + "type": [ + "string", + "null" + ] + }, + "longDescription": { + "description": "The argument long description which will be shown on the help information. Typically this a more detailed (multi-line) message that describes the argument.", + "type": [ + "string", + "null" + ] + }, + "takesValue": { + "description": "Specifies that the argument takes a value at run time.\n\nNOTE: values for arguments may be specified in any of the following methods - Using a space such as -o value or --option value - Using an equals and no space such as -o=value or --option=value - Use a short and no space such as -ovalue", + "default": false, + "type": "boolean" + }, + "multiple": { + "description": "Specifies that the argument may have an unknown number of multiple values. Without any other settings, this argument may appear only once.\n\nFor example, --opt val1 val2 is allowed, but --opt val1 val2 --opt val3 is not.\n\nNOTE: Setting this requires `takes_value` to be set to true.", + "default": false, + "type": "boolean" + }, + "multipleOccurrences": { + "description": "Specifies that the argument may appear more than once. For flags, this results in the number of occurrences of the flag being recorded. For example -ddd or -d -d -d would count as three occurrences. For options or arguments that take a value, this does not affect how many values they can accept. (i.e. only one at a time is allowed)\n\nFor example, --opt val1 --opt val2 is allowed, but --opt val1 val2 is not.", + "default": false, + "type": "boolean" + }, + "numberOfValues": { + "description": "Specifies how many values are required to satisfy this argument. For example, if you had a `-f ` argument where you wanted exactly 3 'files' you would set `number_of_values = 3`, and this argument wouldn't be satisfied unless the user provided 3 and only 3 values.\n\n**NOTE:** Does *not* require `multiple_occurrences = true` to be set. Setting `multiple_occurrences = true` would allow `-f -f ` where as *not* setting it would only allow one occurrence of this argument.\n\n**NOTE:** implicitly sets `takes_value = true` and `multiple_values = true`.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "possibleValues": { + "description": "Specifies a list of possible values for this argument. At runtime, the CLI verifies that only one of the specified values was used, or fails with an error message.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "minValues": { + "description": "Specifies the minimum number of values for this argument. For example, if you had a -f `` argument where you wanted at least 2 'files', you would set `minValues: 2`, and this argument would be satisfied if the user provided, 2 or more values.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "maxValues": { + "description": "Specifies the maximum number of values are for this argument. For example, if you had a -f `` argument where you wanted up to 3 'files', you would set .max_values(3), and this argument would be satisfied if the user provided, 1, 2, or 3 values.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "required": { + "description": "Sets whether or not the argument is required by default.\n\n- Required by default means it is required, when no other conflicting rules have been evaluated - Conflicting rules take precedence over being required.", + "default": false, + "type": "boolean" + }, + "requiredUnlessPresent": { + "description": "Sets an arg that override this arg's required setting i.e. this arg will be required unless this other argument is present.", + "type": [ + "string", + "null" + ] + }, + "requiredUnlessPresentAll": { + "description": "Sets args that override this arg's required setting i.e. this arg will be required unless all these other arguments are present.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requiredUnlessPresentAny": { + "description": "Sets args that override this arg's required setting i.e. this arg will be required unless at least one of these other arguments are present.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "conflictsWith": { + "description": "Sets a conflicting argument by name i.e. when using this argument, the following argument can't be present and vice versa.", + "type": [ + "string", + "null" + ] + }, + "conflictsWithAll": { + "description": "The same as conflictsWith but allows specifying multiple two-way conflicts per argument.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requires": { + "description": "Tets an argument by name that is required when this one is present i.e. when using this argument, the following argument must be present.", + "type": [ + "string", + "null" + ] + }, + "requiresAll": { + "description": "Sts multiple arguments by names that are required when this one is present i.e. when using this argument, the following arguments must be present.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requiresIf": { + "description": "Allows a conditional requirement with the signature [arg, value] the requirement will only become valid if `arg`'s value equals `${value}`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requiredIfEq": { + "description": "Allows specifying that an argument is required conditionally with the signature [arg, value] the requirement will only become valid if the `arg`'s value equals `${value}`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requireEquals": { + "description": "Requires that options use the --option=val syntax i.e. an equals between the option and associated value.", + "type": [ + "boolean", + "null" + ] + }, + "index": { + "description": "The positional argument index, starting at 1.\n\nThe index refers to position according to other positional argument. It does not define position in the argument list as a whole. When utilized with multiple=true, only the last positional argument may be defined as multiple (i.e. the one with the highest index).", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1.0 + } + }, + "additionalProperties": false + }, + "BundleConfig": { + "description": "Configuration for tauri-bundler.\n\nSee more: https://tauri.app/v1/api/config#bundleconfig", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "active": { + "description": "Whether Tauri should bundle your application or just output the executable.", + "default": false, + "type": "boolean" + }, + "targets": { + "description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".", + "default": "all", + "allOf": [ + { + "$ref": "#/definitions/BundleTarget" + } + ] + }, + "identifier": { + "description": "The application identifier in reverse domain name notation (e.g. `com.tauri.example`). This string must be unique across applications since it is used in system configurations like the bundle ID and path to the webview data directory. This string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.).", + "type": "string" + }, + "publisher": { + "description": "The application's publisher. Defaults to the second element in the identifier string. Currently maps to the Manufacturer property of the Windows Installer.", + "type": [ + "string", + "null" + ] + }, + "icon": { + "description": "The app's icons", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "resources": { + "description": "App resources to bundle. Each resource is a path to a file or directory. Glob patterns are supported.", + "anyOf": [ + { + "$ref": "#/definitions/BundleResources" + }, + { + "type": "null" + } + ] + }, + "copyright": { + "description": "A copyright string associated with your application.", + "type": [ + "string", + "null" + ] + }, + "category": { + "description": "The application kind.\n\nShould be one of the following: Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather.", + "type": [ + "string", + "null" + ] + }, + "shortDescription": { + "description": "A short description of your application.", + "type": [ + "string", + "null" + ] + }, + "longDescription": { + "description": "A longer, multi-line description of the application.", + "type": [ + "string", + "null" + ] + }, + "useLocalToolsDir": { + "description": "Whether to use the project's `target` directory, for caching build tools (e.g., Wix and NSIS) when building this application. Defaults to `false`.\n\nIf true, tools will be cached in `target\\.tauri-tools`. If false, tools will be cached in the current user's platform-specific cache directory.\n\nAn example where it can be appropriate to set this to `true` is when building this application as a Windows System user (e.g., AWS EC2 workloads), because the Window system's app data directory is restricted.", + "default": false, + "type": "boolean" + }, + "appimage": { + "description": "Configuration for the AppImage bundle.", + "default": { + "bundleMediaFramework": false + }, + "allOf": [ + { + "$ref": "#/definitions/AppImageConfig" + } + ] + }, + "deb": { + "description": "Configuration for the Debian bundle.", + "default": { + "files": {} + }, + "allOf": [ + { + "$ref": "#/definitions/DebConfig" + } + ] + }, + "rpm": { + "description": "Configuration for the RPM bundle.", + "default": { + "epoch": 0, + "files": {}, + "release": "1" + }, + "allOf": [ + { + "$ref": "#/definitions/RpmConfig" + } + ] + }, + "dmg": { + "description": "DMG-specific settings.", + "default": { + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + }, + "windowSize": { + "height": 400, + "width": 660 + } + }, + "allOf": [ + { + "$ref": "#/definitions/DmgConfig" + } + ] + }, + "macOS": { + "description": "Configuration for the macOS bundles.", + "default": { + "minimumSystemVersion": "10.13" + }, + "allOf": [ + { + "$ref": "#/definitions/MacConfig" + } + ] + }, + "externalBin": { + "description": "A list of—either absolute or relative—paths to binaries to embed with your application.\n\nNote that Tauri will look for system-specific binaries following the pattern \"binary-name{-target-triple}{.system-extension}\".\n\nE.g. for the external binary \"my-binary\", Tauri looks for:\n\n- \"my-binary-x86_64-pc-windows-msvc.exe\" for Windows - \"my-binary-x86_64-apple-darwin\" for macOS - \"my-binary-x86_64-unknown-linux-gnu\" for Linux\n\nso don't forget to provide binaries for all targeted platforms.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "windows": { + "description": "Configuration for the Windows bundle.", + "default": { + "allowDowngrades": true, + "certificateThumbprint": null, + "digestAlgorithm": null, + "nsis": null, + "signCommand": null, + "timestampUrl": null, + "tsp": false, + "webviewFixedRuntimePath": null, + "webviewInstallMode": { + "silent": true, + "type": "downloadBootstrapper" + }, + "wix": null + }, + "allOf": [ + { + "$ref": "#/definitions/WindowsConfig" + } + ] + } + }, + "additionalProperties": false + }, + "BundleTarget": { + "description": "Targets to bundle. Each value is case insensitive.", + "anyOf": [ + { + "description": "Bundle all targets.", + "enum": [ + "all" + ] + }, + { + "description": "A list of bundle targets.", + "type": "array", + "items": { + "$ref": "#/definitions/BundleType" + } + }, + { + "description": "A single bundle target.", + "allOf": [ + { + "$ref": "#/definitions/BundleType" + } + ] + } + ] + }, + "BundleType": { + "description": "A bundle referenced by tauri-bundler.", + "oneOf": [ + { + "description": "The debian bundle (.deb).", + "type": "string", + "enum": [ + "deb" + ] + }, + { + "description": "The RPM bundle (.rpm).", + "type": "string", + "enum": [ + "rpm" + ] + }, + { + "description": "The AppImage bundle (.appimage).", + "type": "string", + "enum": [ + "appimage" + ] + }, + { + "description": "The Microsoft Installer bundle (.msi).", + "type": "string", + "enum": [ + "msi" + ] + }, + { + "description": "The NSIS bundle (.exe).", + "type": "string", + "enum": [ + "nsis" + ] + }, + { + "description": "The macOS application bundle (.app).", + "type": "string", + "enum": [ + "app" + ] + }, + { + "description": "The Apple Disk Image bundle (.dmg).", + "type": "string", + "enum": [ + "dmg" + ] + }, + { + "description": "The Tauri updater bundle.", + "type": "string", + "enum": [ + "updater" + ] + } + ] + }, + "BundleResources": { + "description": "Definition for bundle resources. Can be either a list of paths to include or a map of source to target paths.", + "anyOf": [ + { + "description": "A list of paths to include.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "A map of source to target paths.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ] + }, + "AppImageConfig": { + "description": "Configuration for AppImage bundles.\n\nSee more: https://tauri.app/v1/api/config#appimageconfig", + "type": "object", + "properties": { + "bundleMediaFramework": { + "description": "Include additional gstreamer dependencies needed for audio and video playback. This increases the bundle size by ~15-35MB depending on your build system.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "DebConfig": { + "description": "Configuration for Debian (.deb) bundles.\n\nSee more: https://tauri.app/v1/api/config#debconfig", + "type": "object", + "properties": { + "depends": { + "description": "The list of deb dependencies your application relies on.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "provides": { + "description": "The list of dependencies the package provides.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "conflicts": { + "description": "The list of package conflicts.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "replaces": { + "description": "The list of package replaces.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "files": { + "description": "The files to include on the package.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "desktopTemplate": { + "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.", + "type": [ + "string", + "null" + ] + }, + "section": { + "description": "Define the section in Debian Control file. See : https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections", + "type": [ + "string", + "null" + ] + }, + "priority": { + "description": "Change the priority of the Debian Package. By default, it is set to `optional`. Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra`", + "type": [ + "string", + "null" + ] + }, + "changelog": { + "description": "Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "RpmConfig": { + "description": "Configuration for RPM bundles.", + "type": "object", + "properties": { + "license": { + "description": "The package's license identifier. If not set, defaults to the license from the Cargo.toml file.", + "type": [ + "string", + "null" + ] + }, + "depends": { + "description": "The list of RPM dependencies your application relies on.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "provides": { + "description": "The list of RPM dependencies your application provides.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "conflicts": { + "description": "The list of RPM dependencies your application conflicts with. They must not be present in order for the package to be installed.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "obsoletes": { + "description": "The list of RPM dependencies your application supersedes - if this package is installed, packages listed as “obsoletes” will be automatically removed (if they are present).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "release": { + "description": "The RPM release tag.", + "default": "1", + "type": "string" + }, + "epoch": { + "description": "The RPM epoch.", + "default": 0, + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "files": { + "description": "The files to include on the package.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "desktopTemplate": { + "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "DmgConfig": { + "description": "Configuration for Apple Disk Image (.dmg) bundles.\n\nSee more: https://tauri.app/v1/api/config#dmgconfig", + "type": "object", + "properties": { + "background": { + "description": "Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`.", + "type": [ + "string", + "null" + ] + }, + "windowPosition": { + "description": "Position of volume window on screen.", + "anyOf": [ + { + "$ref": "#/definitions/Position" + }, + { + "type": "null" + } + ] + }, + "windowSize": { + "description": "Size of volume window.", + "default": { + "height": 400, + "width": 660 + }, + "allOf": [ + { + "$ref": "#/definitions/Size" + } + ] + }, + "appPosition": { + "description": "Position of app file on window.", + "default": { + "x": 180, + "y": 170 + }, + "allOf": [ + { + "$ref": "#/definitions/Position" + } + ] + }, + "applicationFolderPosition": { + "description": "Position of application folder on window.", + "default": { + "x": 480, + "y": 170 + }, + "allOf": [ + { + "$ref": "#/definitions/Position" + } + ] + } + }, + "additionalProperties": false + }, + "Position": { + "description": "Position coordinates struct.", + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "description": "X coordinate.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "y": { + "description": "Y coordinate.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Size": { + "description": "Size of the window.", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Width of the window.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Height of the window.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "MacConfig": { + "description": "Configuration for the macOS bundles.\n\nSee more: https://tauri.app/v1/api/config#macconfig", + "type": "object", + "properties": { + "frameworks": { + "description": "A list of strings indicating any macOS X frameworks that need to be bundled with the application.\n\nIf a name is used, \".framework\" must be omitted and it will look for standard install locations. You may also use a path to a specific framework.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "minimumSystemVersion": { + "description": "A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.\n\nSetting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist` and the `MACOSX_DEPLOYMENT_TARGET` environment variable.\n\nAn empty string is considered an invalid value so the default value is used.", + "default": "10.13", + "type": [ + "string", + "null" + ] + }, + "exceptionDomain": { + "description": "Allows your application to communicate with the outside world. It should be a lowercase, without port and protocol domain name.", + "type": [ + "string", + "null" + ] + }, + "license": { + "description": "The path to the license file to add to the DMG bundle.", + "type": [ + "string", + "null" + ] + }, + "signingIdentity": { + "description": "Identity to use for code signing.", + "type": [ + "string", + "null" + ] + }, + "providerShortName": { + "description": "Provider short name for notarization.", + "type": [ + "string", + "null" + ] + }, + "entitlements": { + "description": "Path to the entitlements file.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "WindowsConfig": { + "description": "Windows bundler configuration.\n\nSee more: https://tauri.app/v1/api/config#windowsconfig", + "type": "object", + "properties": { + "digestAlgorithm": { + "description": "Specifies the file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.", + "type": [ + "string", + "null" + ] + }, + "certificateThumbprint": { + "description": "Specifies the SHA1 hash of the signing certificate.", + "type": [ + "string", + "null" + ] + }, + "timestampUrl": { + "description": "Server to use during timestamping.", + "type": [ + "string", + "null" + ] + }, + "tsp": { + "description": "Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.", + "default": false, + "type": "boolean" + }, + "webviewInstallMode": { + "description": "The installation mode for the Webview2 runtime.", + "default": { + "silent": true, + "type": "downloadBootstrapper" + }, + "allOf": [ + { + "$ref": "#/definitions/WebviewInstallMode" + } + ] + }, + "webviewFixedRuntimePath": { + "description": "Path to the webview fixed runtime to use. Overwrites [`Self::webview_install_mode`] if set.\n\nWill be removed in v2, prefer the [`Self::webview_install_mode`] option.\n\nThe fixed version can be downloaded [on the official website](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section). The `.cab` file must be extracted to a folder and this folder path must be defined on this field.", + "type": [ + "string", + "null" + ] + }, + "allowDowngrades": { + "description": "Validates a second app installation, blocking the user from installing an older version if set to `false`.\n\nFor instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.\n\nThe default value of this flag is `true`.", + "default": true, + "type": "boolean" + }, + "wix": { + "description": "Configuration for the MSI generated with WiX.", + "anyOf": [ + { + "$ref": "#/definitions/WixConfig" + }, + { + "type": "null" + } + ] + }, + "nsis": { + "description": "Configuration for the installer generated with NSIS.", + "anyOf": [ + { + "$ref": "#/definitions/NsisConfig" + }, + { + "type": "null" + } + ] + }, + "signCommand": { + "description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nExample: ```text sign-cli --arg1 --arg2 %1 ```\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "WebviewInstallMode": { + "description": "Install modes for the Webview2 runtime. Note that for the updater bundle [`Self::DownloadBootstrapper`] is used.\n\nFor more information see .", + "oneOf": [ + { + "description": "Do not install the Webview2 as part of the Windows Installer.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "skip" + ] + } + }, + "additionalProperties": false + }, + { + "description": "Download the bootstrapper and run it. Requires an internet connection. Results in a smaller installer size, but is not recommended on Windows 7.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "downloadBootstrapper" + ] + }, + "silent": { + "description": "Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + { + "description": "Embed the bootstrapper and run it. Requires an internet connection. Increases the installer size by around 1.8MB, but offers better support on Windows 7.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "embedBootstrapper" + ] + }, + "silent": { + "description": "Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + { + "description": "Embed the offline installer and run it. Does not require an internet connection. Increases the installer size by around 127MB.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "offlineInstaller" + ] + }, + "silent": { + "description": "Instructs the installer to run the installer in silent mode. Defaults to `true`.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + { + "description": "Embed a fixed webview2 version and use it at runtime. Increases the installer size by around 180MB.", + "type": "object", + "required": [ + "path", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixedRuntime" + ] + }, + "path": { + "description": "The path to the fixed runtime to use.\n\nThe fixed version can be downloaded [on the official website](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section). The `.cab` file must be extracted to a folder and this folder path must be defined on this field.", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "WixConfig": { + "description": "Configuration for the MSI bundle using WiX.\n\nSee more: https://tauri.app/v1/api/config#wixconfig", + "type": "object", + "properties": { + "language": { + "description": "The installer languages to build. See .", + "default": "en-US", + "allOf": [ + { + "$ref": "#/definitions/WixLanguage" + } + ] + }, + "template": { + "description": "A custom .wxs template to use.", + "type": [ + "string", + "null" + ] + }, + "fragmentPaths": { + "description": "A list of paths to .wxs files with WiX fragments to use.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "componentGroupRefs": { + "description": "The ComponentGroup element ids you want to reference from the fragments.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "componentRefs": { + "description": "The Component element ids you want to reference from the fragments.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "featureGroupRefs": { + "description": "The FeatureGroup element ids you want to reference from the fragments.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "featureRefs": { + "description": "The Feature element ids you want to reference from the fragments.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "mergeRefs": { + "description": "The Merge element ids you want to reference from the fragments.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "skipWebviewInstall": { + "description": "Disables the Webview2 runtime installation after app install.\n\nWill be removed in v2, prefer the [`WindowsConfig::webview_install_mode`] option.", + "default": false, + "type": "boolean" + }, + "license": { + "description": "The path to the license file to render on the installer.\n\nMust be an RTF file, so if a different extension is provided, we convert it to the RTF format.", + "type": [ + "string", + "null" + ] + }, + "enableElevatedUpdateTask": { + "description": "Create an elevated update task within Windows Task Scheduler.", + "default": false, + "type": "boolean" + }, + "bannerPath": { + "description": "Path to a bitmap file to use as the installation user interface banner. This bitmap will appear at the top of all but the first page of the installer.\n\nThe required dimensions are 493px × 58px.", + "type": [ + "string", + "null" + ] + }, + "dialogImagePath": { + "description": "Path to a bitmap file to use on the installation user interface dialogs. It is used on the welcome and completion dialogs. The required dimensions are 493px × 312px.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "WixLanguage": { + "description": "The languages to build using WiX.", + "anyOf": [ + { + "description": "A single language to build, without configuration.", + "type": "string" + }, + { + "description": "A list of languages to build, without configuration.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "A map of languages and its configuration.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/WixLanguageConfig" + } + } + ] + }, + "WixLanguageConfig": { + "description": "Configuration for a target language for the WiX build.\n\nSee more: https://tauri.app/v1/api/config#wixlanguageconfig", + "type": "object", + "properties": { + "localePath": { + "description": "The path to a locale (`.wxl`) file. See .", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "NsisConfig": { + "description": "Configuration for the Installer bundle using NSIS.", + "type": "object", + "properties": { + "template": { + "description": "A custom .nsi template to use.", + "type": [ + "string", + "null" + ] + }, + "license": { + "description": "The path to the license file to render on the installer.", + "type": [ + "string", + "null" + ] + }, + "headerImage": { + "description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, + "sidebarImage": { + "description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.", + "type": [ + "string", + "null" + ] + }, + "installerIcon": { + "description": "The path to an icon file used as the installer icon.", + "type": [ + "string", + "null" + ] + }, + "installMode": { + "description": "Whether the installation will be for all users or just the current user.", + "default": "currentUser", + "allOf": [ + { + "$ref": "#/definitions/NSISInstallerMode" + } + ] + }, + "languages": { + "description": "A list of installer languages. By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. To allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "customLanguageFiles": { + "description": "A key-value pair where the key is the language and the value is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.\n\nSee for an example `.nsh` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`] languages array,", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "displayLanguageSelector": { + "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.", + "default": false, + "type": "boolean" + }, + "compression": { + "description": "Set the compression algorithm used to compress files in the installer.\n\nSee ", + "anyOf": [ + { + "$ref": "#/definitions/NsisCompression" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "NSISInstallerMode": { + "description": "Install Modes for the NSIS installer.", + "oneOf": [ + { + "description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.", + "type": "string", + "enum": [ + "currentUser" + ] + }, + { + "description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.", + "type": "string", + "enum": [ + "perMachine" + ] + }, + { + "description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.", + "type": "string", + "enum": [ + "both" + ] + } + ] + }, + "NsisCompression": { + "description": "Compression algorithms used in the NSIS installer.\n\nSee ", + "oneOf": [ + { + "description": "ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.", + "type": "string", + "enum": [ + "zlib" + ] + }, + { + "description": "BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.", + "type": "string", + "enum": [ + "bzip2" + ] + }, + { + "description": "LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB.", + "type": "string", + "enum": [ + "lzma" + ] + } + ] + }, + "AllowlistConfig": { + "description": "Allowlist configuration. The allowlist is a translation of the [Cargo allowlist features](https://docs.rs/tauri/latest/tauri/#cargo-allowlist-features).\n\n# Notes\n\n- Endpoints that don't have their own allowlist option are enabled by default. - There is only \"opt-in\", no \"opt-out\". Setting an option to `false` has no effect.\n\n# Examples\n\n- * [`\"app-all\": true`](https://tauri.app/v1/api/config/#appallowlistconfig.all) will make the [hide](https://tauri.app/v1/api/js/app#hide) endpoint be available regardless of whether `hide` is set to `false` or `true` in the allowlist.", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all API features.", + "default": false, + "type": "boolean" + }, + "fs": { + "description": "File system API allowlist.", + "default": { + "all": false, + "copyFile": false, + "createDir": false, + "exists": false, + "readDir": false, + "readFile": false, + "removeDir": false, + "removeFile": false, + "renameFile": false, + "scope": [], + "writeFile": false + }, + "allOf": [ + { + "$ref": "#/definitions/FsAllowlistConfig" + } + ] + }, + "window": { + "description": "Window API allowlist.", + "default": { + "all": false, + "center": false, + "close": false, + "create": false, + "hide": false, + "maximize": false, + "minimize": false, + "print": false, + "requestUserAttention": false, + "setAlwaysOnTop": false, + "setClosable": false, + "setContentProtected": false, + "setCursorGrab": false, + "setCursorIcon": false, + "setCursorPosition": false, + "setCursorVisible": false, + "setDecorations": false, + "setFocus": false, + "setFullscreen": false, + "setIcon": false, + "setIgnoreCursorEvents": false, + "setMaxSize": false, + "setMaximizable": false, + "setMinSize": false, + "setMinimizable": false, + "setPosition": false, + "setResizable": false, + "setSize": false, + "setSkipTaskbar": false, + "setTitle": false, + "show": false, + "startDragging": false, + "unmaximize": false, + "unminimize": false + }, + "allOf": [ + { + "$ref": "#/definitions/WindowAllowlistConfig" + } + ] + }, + "shell": { + "description": "Shell API allowlist.", + "default": { + "all": false, + "execute": false, + "open": false, + "scope": [], + "sidecar": false + }, + "allOf": [ + { + "$ref": "#/definitions/ShellAllowlistConfig" + } + ] + }, + "dialog": { + "description": "Dialog API allowlist.", + "default": { + "all": false, + "ask": false, + "confirm": false, + "message": false, + "open": false, + "save": false + }, + "allOf": [ + { + "$ref": "#/definitions/DialogAllowlistConfig" + } + ] + }, + "http": { + "description": "HTTP API allowlist.", + "default": { + "all": false, + "request": false, + "scope": [] + }, + "allOf": [ + { + "$ref": "#/definitions/HttpAllowlistConfig" + } + ] + }, + "notification": { + "description": "Notification API allowlist.", + "default": { + "all": false + }, + "allOf": [ + { + "$ref": "#/definitions/NotificationAllowlistConfig" + } + ] + }, + "globalShortcut": { + "description": "Global shortcut API allowlist.", + "default": { + "all": false + }, + "allOf": [ + { + "$ref": "#/definitions/GlobalShortcutAllowlistConfig" + } + ] + }, + "os": { + "description": "OS allowlist.", + "default": { + "all": false + }, + "allOf": [ + { + "$ref": "#/definitions/OsAllowlistConfig" + } + ] + }, + "path": { + "description": "Path API allowlist.", + "default": { + "all": false + }, + "allOf": [ + { + "$ref": "#/definitions/PathAllowlistConfig" + } + ] + }, + "protocol": { + "description": "Custom protocol allowlist.", + "default": { + "all": false, + "asset": false, + "assetScope": [] + }, + "allOf": [ + { + "$ref": "#/definitions/ProtocolAllowlistConfig" + } + ] + }, + "process": { + "description": "Process API allowlist.", + "default": { + "all": false, + "exit": false, + "relaunch": false, + "relaunchDangerousAllowSymlinkMacos": false + }, + "allOf": [ + { + "$ref": "#/definitions/ProcessAllowlistConfig" + } + ] + }, + "clipboard": { + "description": "Clipboard APIs allowlist.", + "default": { + "all": false, + "readText": false, + "writeText": false + }, + "allOf": [ + { + "$ref": "#/definitions/ClipboardAllowlistConfig" + } + ] + }, + "app": { + "description": "App APIs allowlist.", + "default": { + "all": false, + "hide": false, + "show": false + }, + "allOf": [ + { + "$ref": "#/definitions/AppAllowlistConfig" + } + ] + } + }, + "additionalProperties": false + }, + "FsAllowlistConfig": { + "description": "Allowlist for the file system APIs.\n\nSee more: https://tauri.app/v1/api/config#fsallowlistconfig", + "type": "object", + "properties": { + "scope": { + "description": "The access scope for the filesystem APIs.", + "default": [], + "allOf": [ + { + "$ref": "#/definitions/FsAllowlistScope" + } + ] + }, + "all": { + "description": "Use this flag to enable all file system API features.", + "default": false, + "type": "boolean" + }, + "readFile": { + "description": "Read file from local filesystem.", + "default": false, + "type": "boolean" + }, + "writeFile": { + "description": "Write file to local filesystem.", + "default": false, + "type": "boolean" + }, + "readDir": { + "description": "Read directory from local filesystem.", + "default": false, + "type": "boolean" + }, + "copyFile": { + "description": "Copy file from local filesystem.", + "default": false, + "type": "boolean" + }, + "createDir": { + "description": "Create directory from local filesystem.", + "default": false, + "type": "boolean" + }, + "removeDir": { + "description": "Remove directory from local filesystem.", + "default": false, + "type": "boolean" + }, + "removeFile": { + "description": "Remove file from local filesystem.", + "default": false, + "type": "boolean" + }, + "renameFile": { + "description": "Rename file from local filesystem.", + "default": false, + "type": "boolean" + }, + "exists": { + "description": "Check if path exists on the local filesystem.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "FsAllowlistScope": { + "description": "Filesystem scope definition. It is a list of glob patterns that restrict the API access from the webview.\n\nEach pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "anyOf": [ + { + "description": "A list of paths that are allowed by this scope.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "A complete scope configuration.", + "type": "object", + "properties": { + "allow": { + "description": "A list of paths that are allowed by this scope.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "A list of paths that are not allowed by this scope. This gets precedence over the [`Self::Scope::allow`] list.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "requireLiteralLeadingDot": { + "description": "Whether or not paths that contain components that start with a `.` will require that `.` appears literally in the pattern; `*`, `?`, `**`, or `[...]` will not match. This is useful because such files are conventionally considered hidden on Unix systems and it might be desirable to skip them when listing files.\n\nDefaults to `true` on Unix systems and `false` on Windows", + "type": [ + "boolean", + "null" + ] + } + } + } + ] + }, + "WindowAllowlistConfig": { + "description": "Allowlist for the window APIs.\n\nSee more: https://tauri.app/v1/api/config#windowallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all window API features.", + "default": false, + "type": "boolean" + }, + "create": { + "description": "Allows dynamic window creation.", + "default": false, + "type": "boolean" + }, + "center": { + "description": "Allows centering the window.", + "default": false, + "type": "boolean" + }, + "requestUserAttention": { + "description": "Allows requesting user attention on the window.", + "default": false, + "type": "boolean" + }, + "setResizable": { + "description": "Allows setting the resizable flag of the window.", + "default": false, + "type": "boolean" + }, + "setMaximizable": { + "description": "Allows setting whether the window's native maximize button is enabled or not.", + "default": false, + "type": "boolean" + }, + "setMinimizable": { + "description": "Allows setting whether the window's native minimize button is enabled or not.", + "default": false, + "type": "boolean" + }, + "setClosable": { + "description": "Allows setting whether the window's native close button is enabled or not.", + "default": false, + "type": "boolean" + }, + "setTitle": { + "description": "Allows changing the window title.", + "default": false, + "type": "boolean" + }, + "maximize": { + "description": "Allows maximizing the window.", + "default": false, + "type": "boolean" + }, + "unmaximize": { + "description": "Allows unmaximizing the window.", + "default": false, + "type": "boolean" + }, + "minimize": { + "description": "Allows minimizing the window.", + "default": false, + "type": "boolean" + }, + "unminimize": { + "description": "Allows unminimizing the window.", + "default": false, + "type": "boolean" + }, + "show": { + "description": "Allows showing the window.", + "default": false, + "type": "boolean" + }, + "hide": { + "description": "Allows hiding the window.", + "default": false, + "type": "boolean" + }, + "close": { + "description": "Allows closing the window.", + "default": false, + "type": "boolean" + }, + "setDecorations": { + "description": "Allows setting the decorations flag of the window.", + "default": false, + "type": "boolean" + }, + "setAlwaysOnTop": { + "description": "Allows setting the always_on_top flag of the window.", + "default": false, + "type": "boolean" + }, + "setContentProtected": { + "description": "Allows preventing the window contents from being captured by other apps.", + "default": false, + "type": "boolean" + }, + "setSize": { + "description": "Allows setting the window size.", + "default": false, + "type": "boolean" + }, + "setMinSize": { + "description": "Allows setting the window minimum size.", + "default": false, + "type": "boolean" + }, + "setMaxSize": { + "description": "Allows setting the window maximum size.", + "default": false, + "type": "boolean" + }, + "setPosition": { + "description": "Allows changing the position of the window.", + "default": false, + "type": "boolean" + }, + "setFullscreen": { + "description": "Allows setting the fullscreen flag of the window.", + "default": false, + "type": "boolean" + }, + "setFocus": { + "description": "Allows focusing the window.", + "default": false, + "type": "boolean" + }, + "setIcon": { + "description": "Allows changing the window icon.", + "default": false, + "type": "boolean" + }, + "setSkipTaskbar": { + "description": "Allows setting the skip_taskbar flag of the window.", + "default": false, + "type": "boolean" + }, + "setCursorGrab": { + "description": "Allows grabbing the cursor.", + "default": false, + "type": "boolean" + }, + "setCursorVisible": { + "description": "Allows setting the cursor visibility.", + "default": false, + "type": "boolean" + }, + "setCursorIcon": { + "description": "Allows changing the cursor icon.", + "default": false, + "type": "boolean" + }, + "setCursorPosition": { + "description": "Allows setting the cursor position.", + "default": false, + "type": "boolean" + }, + "setIgnoreCursorEvents": { + "description": "Allows ignoring cursor events.", + "default": false, + "type": "boolean" + }, + "startDragging": { + "description": "Allows start dragging on the window.", + "default": false, + "type": "boolean" + }, + "print": { + "description": "Allows opening the system dialog to print the window content.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ShellAllowlistConfig": { + "description": "Allowlist for the shell APIs.\n\nSee more: https://tauri.app/v1/api/config#shellallowlistconfig", + "type": "object", + "properties": { + "scope": { + "description": "Access scope for the binary execution APIs. Sidecars are automatically enabled.", + "default": [], + "allOf": [ + { + "$ref": "#/definitions/ShellAllowlistScope" + } + ] + }, + "all": { + "description": "Use this flag to enable all shell API features.", + "default": false, + "type": "boolean" + }, + "execute": { + "description": "Enable binary execution.", + "default": false, + "type": "boolean" + }, + "sidecar": { + "description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar command, an executable that is shipped with the application. For more information see .", + "default": false, + "type": "boolean" + }, + "open": { + "description": "Open URL with the user's default application.", + "default": false, + "allOf": [ + { + "$ref": "#/definitions/ShellAllowlistOpen" + } + ] + } + }, + "additionalProperties": false + }, + "ShellAllowlistScope": { + "description": "Shell scope definition. It is a list of command names and associated CLI arguments that restrict the API access from the webview.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellAllowedCommand" + } + }, + "ShellAllowedCommand": { + "description": "A command allowed to be executed by the webview API.", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", + "type": "string" + }, + "cmd": { + "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "default": "", + "type": "string" + }, + "args": { + "description": "The allowed arguments for the command execution.", + "default": false, + "allOf": [ + { + "$ref": "#/definitions/ShellAllowedArgs" + } + ] + }, + "sidecar": { + "description": "If this command is a sidecar command.", + "default": false, + "type": "boolean" + } + } + }, + "ShellAllowedArgs": { + "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", + "anyOf": [ + { + "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", + "type": "boolean" + }, + { + "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellAllowedArg" + } + } + ] + }, + "ShellAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "validator": { + "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax", + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "ShellAllowlistOpen": { + "description": "Defines the `shell > open` api scope.", + "anyOf": [ + { + "description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+`) is used.", + "type": "boolean" + }, + { + "description": "Enable the shell open API, with a custom regex that the opened path must match against.\n\nIf using a custom regex to support a non-http(s) schema, care should be used to prevent values that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.", + "type": "string" + } + ] + }, + "DialogAllowlistConfig": { + "description": "Allowlist for the dialog APIs.\n\nSee more: https://tauri.app/v1/api/config#dialogallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all dialog API features.", + "default": false, + "type": "boolean" + }, + "open": { + "description": "Allows the API to open a dialog window to pick files.", + "default": false, + "type": "boolean" + }, + "save": { + "description": "Allows the API to open a dialog window to pick where to save files.", + "default": false, + "type": "boolean" + }, + "message": { + "description": "Allows the API to show a message dialog window.", + "default": false, + "type": "boolean" + }, + "ask": { + "description": "Allows the API to show a dialog window with Yes/No buttons.", + "default": false, + "type": "boolean" + }, + "confirm": { + "description": "Allows the API to show a dialog window with Ok/Cancel buttons.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "HttpAllowlistConfig": { + "description": "Allowlist for the HTTP APIs.\n\nSee more: https://tauri.app/v1/api/config#httpallowlistconfig", + "type": "object", + "properties": { + "scope": { + "description": "The access scope for the HTTP APIs.", + "default": [], + "allOf": [ + { + "$ref": "#/definitions/HttpAllowlistScope" + } + ] + }, + "all": { + "description": "Use this flag to enable all HTTP API features.", + "default": false, + "type": "boolean" + }, + "request": { + "description": "Allows making HTTP requests.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "HttpAllowlistScope": { + "description": "HTTP API scope definition. It is a list of URLs that can be accessed by the webview when using the HTTP APIs. The scoped URL is matched against the request URL using a glob pattern.\n\nExamples: - \"https://*\": allows all HTTPS urls - \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path - \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "NotificationAllowlistConfig": { + "description": "Allowlist for the notification APIs.\n\nSee more: https://tauri.app/v1/api/config#notificationallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all notification API features.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "GlobalShortcutAllowlistConfig": { + "description": "Allowlist for the global shortcut APIs.\n\nSee more: https://tauri.app/v1/api/config#globalshortcutallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all global shortcut API features.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "OsAllowlistConfig": { + "description": "Allowlist for the OS APIs.\n\nSee more: https://tauri.app/v1/api/config#osallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all OS API features.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PathAllowlistConfig": { + "description": "Allowlist for the path APIs.\n\nSee more: https://tauri.app/v1/api/config#pathallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all path API features.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProtocolAllowlistConfig": { + "description": "Allowlist for the custom protocols.\n\nSee more: https://tauri.app/v1/api/config#protocolallowlistconfig", + "type": "object", + "properties": { + "assetScope": { + "description": "The access scope for the asset protocol.", + "default": [], + "allOf": [ + { + "$ref": "#/definitions/FsAllowlistScope" + } + ] + }, + "all": { + "description": "Use this flag to enable all custom protocols.", + "default": false, + "type": "boolean" + }, + "asset": { + "description": "Enables the asset protocol.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProcessAllowlistConfig": { + "description": "Allowlist for the process APIs.\n\nSee more: https://tauri.app/v1/api/config#processallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all process APIs.", + "default": false, + "type": "boolean" + }, + "relaunch": { + "description": "Enables the relaunch API.", + "default": false, + "type": "boolean" + }, + "relaunchDangerousAllowSymlinkMacos": { + "description": "Dangerous option that allows macOS to relaunch even if the binary contains a symlink.\n\nThis is due to macOS having less symlink protection. Highly recommended to not set this flag unless you have a very specific reason too, and understand the implications of it.", + "default": false, + "type": "boolean" + }, + "exit": { + "description": "Enables the exit API.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ClipboardAllowlistConfig": { + "description": "Allowlist for the clipboard APIs.\n\nSee more: https://tauri.app/v1/api/config#clipboardallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all clipboard APIs.", + "default": false, + "type": "boolean" + }, + "writeText": { + "description": "Enables the clipboard's `writeText` API.", + "default": false, + "type": "boolean" + }, + "readText": { + "description": "Enables the clipboard's `readText` API.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "AppAllowlistConfig": { + "description": "Allowlist for the app APIs.\n\nSee more: https://tauri.app/v1/api/config#appallowlistconfig", + "type": "object", + "properties": { + "all": { + "description": "Use this flag to enable all app APIs.", + "default": false, + "type": "boolean" + }, + "show": { + "description": "Enables the app's `show` API.", + "default": false, + "type": "boolean" + }, + "hide": { + "description": "Enables the app's `hide` API.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "SecurityConfig": { + "description": "Security configuration.\n\nSee more: https://tauri.app/v1/api/config#securityconfig", + "type": "object", + "properties": { + "csp": { + "description": "The Content Security Policy that will be injected on all HTML files on the built application. If [`dev_csp`](#SecurityConfig.devCsp) is not specified, this value is also injected on dev.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See .", + "anyOf": [ + { + "$ref": "#/definitions/Csp" + }, + { + "type": "null" + } + ] + }, + "devCsp": { + "description": "The Content Security Policy that will be injected on all HTML files on development.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See .", + "anyOf": [ + { + "$ref": "#/definitions/Csp" + }, + { + "type": "null" + } + ] + }, + "freezePrototype": { + "description": "Freeze the `Object.prototype` when using the custom protocol.", + "default": false, + "type": "boolean" + }, + "dangerousDisableAssetCspModification": { + "description": "Disables the Tauri-injected CSP sources.\n\nAt compile time, Tauri parses all the frontend assets and changes the Content-Security-Policy to only allow loading of your own scripts and styles by injecting nonce and hash sources. This stricts your CSP, which may introduce issues when using along with other flexing sources.\n\nThis configuration option allows both a boolean and a list of strings as value. A boolean instructs Tauri to disable the injection for all CSP injections, and a list of strings indicates the CSP directives that Tauri cannot inject.\n\n**WARNING:** Only disable this if you know what you are doing and have properly configured the CSP. Your application might be vulnerable to XSS attacks without this Tauri protection.", + "default": false, + "allOf": [ + { + "$ref": "#/definitions/DisabledCspModificationKind" + } + ] + }, + "dangerousRemoteDomainIpcAccess": { + "description": "Allow external domains to send command to Tauri.\n\nBy default, external domains do not have access to `window.__TAURI__`, which means they cannot communicate with the commands defined in Rust. This prevents attacks where an externally loaded malicious or compromised sites could start executing commands on the user's device.\n\nThis configuration allows a set of external domains to have access to the Tauri commands. When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed.\n\n**WARNING:** Only use this option if you either have internal checks against malicious external sites or you can trust the allowed external sites. You application might be vulnerable to dangerous Tauri command related attacks otherwise.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/RemoteDomainAccessScope" + } + }, + "dangerousUseHttpScheme": { + "description": "Sets whether the custom protocols should use `http://.localhost` instead of the default `https://.localhost` on Windows.\n\n**WARNING:** Using a `http` scheme will allow mixed content when trying to fetch `http` endpoints and is therefore less secure but will match the behavior of the `://localhost` protocols used on macOS and Linux.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Csp": { + "description": "A Content-Security-Policy definition. See .", + "anyOf": [ + { + "description": "The entire CSP policy in a single text string.", + "type": "string" + }, + { + "description": "An object mapping a directive with its sources values as a list of strings.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/CspDirectiveSources" + } + } + ] + }, + "CspDirectiveSources": { + "description": "A Content-Security-Policy directive source list. See .", + "anyOf": [ + { + "description": "An inline list of CSP sources. Same as [`Self::List`], but concatenated with a space separator.", + "type": "string" + }, + { + "description": "A list of CSP sources. The collection will be concatenated with a space separator for the CSP string.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "DisabledCspModificationKind": { + "description": "The possible values for the `dangerous_disable_asset_csp_modification` config option.", + "anyOf": [ + { + "description": "If `true`, disables all CSP modification. `false` is the default value and it configures Tauri to control the CSP.", + "type": "boolean" + }, + { + "description": "Disables the given list of CSP directives modifications.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "RemoteDomainAccessScope": { + "description": "External command access definition.", + "type": "object", + "required": [ + "domain", + "windows" + ], + "properties": { + "scheme": { + "description": "The URL scheme to allow. By default, all schemas are allowed.", + "type": [ + "string", + "null" + ] + }, + "domain": { + "description": "The domain to allow.", + "type": "string" + }, + "windows": { + "description": "The list of window labels this scope applies to.", + "type": "array", + "items": { + "type": "string" + } + }, + "plugins": { + "description": "The list of plugins that are allowed in this scope. The names should be without the `tauri-plugin-` prefix, for example `\"store\"` for `tauri-plugin-store`.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "enableTauriAPI": { + "description": "Enables access to the Tauri API.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "UpdaterConfig": { + "description": "The Updater configuration object.\n\nSee more: https://tauri.app/v1/api/config#updaterconfig", + "type": "object", + "properties": { + "active": { + "description": "Whether the updater is active or not.", + "default": false, + "type": "boolean" + }, + "dialog": { + "description": "Display built-in dialog or use event system if disabled.", + "default": true, + "type": "boolean" + }, + "endpoints": { + "description": "The updater endpoints. TLS is enforced on production.\n\nThe updater URL can contain the following variables: - {{current_version}}: The version of the app that is requesting the update - {{target}}: The operating system name (one of `linux`, `windows` or `darwin`). - {{arch}}: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`).\n\n# Examples - \"https://my.cdn.com/latest.json\": a raw JSON endpoint that returns the latest version and download links for each platform. - \"https://updates.app.dev/{{target}}?version={{current_version}}&arch={{arch}}\": a dedicated API with positional and query string arguments.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/UpdaterEndpoint" + } + }, + "pubkey": { + "description": "Signature public key.", + "default": "", + "type": "string" + }, + "windows": { + "description": "The Windows configuration for the updater.", + "default": { + "installMode": "passive", + "installerArgs": [] + }, + "allOf": [ + { + "$ref": "#/definitions/UpdaterWindowsConfig" + } + ] + } + }, + "additionalProperties": false + }, + "UpdaterEndpoint": { + "description": "A URL to an updater server.\n\nThe URL must use the `https` scheme on production.", + "type": "string", + "format": "uri" + }, + "UpdaterWindowsConfig": { + "description": "The updater configuration for Windows.\n\nSee more: https://tauri.app/v1/api/config#updaterwindowsconfig", + "type": "object", + "properties": { + "installerArgs": { + "description": "Additional arguments given to the NSIS or WiX installer.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "installMode": { + "description": "The installation mode for the update on Windows. Defaults to `passive`.", + "default": "passive", + "allOf": [ + { + "$ref": "#/definitions/WindowsUpdateInstallMode" + } + ] + } + }, + "additionalProperties": false + }, + "WindowsUpdateInstallMode": { + "description": "Install modes for the Windows update.", + "oneOf": [ + { + "description": "Specifies there's a basic UI during the installation process, including a final dialog box at the end.", + "type": "string", + "enum": [ + "basicUi" + ] + }, + { + "description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does.", + "type": "string", + "enum": [ + "quiet" + ] + }, + { + "description": "Specifies unattended mode, which means the installation only shows a progress bar.", + "type": "string", + "enum": [ + "passive" + ] + } + ] + }, + "SystemTrayConfig": { + "description": "Configuration for application system tray icon.\n\nSee more: https://tauri.app/v1/api/config#systemtrayconfig", + "type": "object", + "required": [ + "iconPath" + ], + "properties": { + "iconPath": { + "description": "Path to the default icon to use on the system tray.", + "type": "string" + }, + "iconAsTemplate": { + "description": "A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.", + "default": false, + "type": "boolean" + }, + "menuOnLeftClick": { + "description": "A Boolean value that determines whether the menu should appear when the tray icon receives a left click on macOS.", + "default": true, + "type": "boolean" + }, + "title": { + "description": "Title for MacOS tray", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "BuildConfig": { + "description": "The Build configuration object.\n\nSee more: https://tauri.app/v1/api/config#buildconfig", + "type": "object", + "properties": { + "runner": { + "description": "The binary used to build and run the application.", + "type": [ + "string", + "null" + ] + }, + "devPath": { + "description": "The path to the application assets or URL to load in development.\n\nThis is usually an URL to a dev server, which serves your application assets with live reloading. Most modern JavaScript bundlers provides a way to start a dev server by default.\n\nSee [vite](https://vitejs.dev/guide/), [Webpack DevServer](https://webpack.js.org/configuration/dev-server/) and [sirv](https://github.com/lukeed/sirv) for examples on how to set up a dev server.", + "default": "http://localhost:8080/", + "allOf": [ + { + "$ref": "#/definitions/AppUrl" + } + ] + }, + "distDir": { + "description": "The path to the application assets or URL to load in production.\n\nWhen a path relative to the configuration file is provided, it is read recursively and all files are embedded in the application binary. Tauri then looks for an `index.html` file unless you provide a custom window URL.\n\nYou can also provide a list of paths to be embedded, which allows granular control over what files are added to the binary. In this case, all files are added to the root and you must reference it that way in your HTML files.\n\nWhen an URL is provided, the application won't have bundled assets and the application will load that URL by default.", + "default": "../dist", + "allOf": [ + { + "$ref": "#/definitions/AppUrl" + } + ] + }, + "beforeDevCommand": { + "description": "A shell command to run before `tauri dev` kicks in.\n\nThe TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG environment variables are set if you perform conditional compilation.", + "anyOf": [ + { + "$ref": "#/definitions/BeforeDevCommand" + }, + { + "type": "null" + } + ] + }, + "beforeBuildCommand": { + "description": "A shell command to run before `tauri build` kicks in.\n\nThe TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG environment variables are set if you perform conditional compilation.", + "anyOf": [ + { + "$ref": "#/definitions/HookCommand" + }, + { + "type": "null" + } + ] + }, + "beforeBundleCommand": { + "description": "A shell command to run before the bundling phase in `tauri build` kicks in.\n\nThe TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG environment variables are set if you perform conditional compilation.", + "anyOf": [ + { + "$ref": "#/definitions/HookCommand" + }, + { + "type": "null" + } + ] + }, + "features": { + "description": "Features passed to `cargo` commands.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "withGlobalTauri": { + "description": "Whether we should inject the Tauri API on `window.__TAURI__` or not.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "AppUrl": { + "description": "Defines the URL or assets to embed in the application.", + "anyOf": [ + { + "description": "The app's external URL, or the path to the directory containing the app assets.", + "allOf": [ + { + "$ref": "#/definitions/WindowUrl" + } + ] + }, + { + "description": "An array of files to embed on the app.", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "BeforeDevCommand": { + "description": "Describes the shell command to run before `tauri dev`.", + "anyOf": [ + { + "description": "Run the given script with the default options.", + "type": "string" + }, + { + "description": "Run the given script with custom options.", + "type": "object", + "required": [ + "script" + ], + "properties": { + "script": { + "description": "The script to execute.", + "type": "string" + }, + "cwd": { + "description": "The current working directory.", + "type": [ + "string", + "null" + ] + }, + "wait": { + "description": "Whether `tauri dev` should wait for the command to finish or not. Defaults to `false`.", + "default": false, + "type": "boolean" + } + } + } + ] + }, + "HookCommand": { + "description": "Describes a shell command to be executed when a CLI hook is triggered.", + "anyOf": [ + { + "description": "Run the given script with the default options.", + "type": "string" + }, + { + "description": "Run the given script with custom options.", + "type": "object", + "required": [ + "script" + ], + "properties": { + "script": { + "description": "The script to execute.", + "type": "string" + }, + "cwd": { + "description": "The current working directory.", + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "PluginConfig": { + "description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\nSee more: https://tauri.app/v1/api/config#pluginconfig", + "type": "object", + "additionalProperties": true + } + } +} \ No newline at end of file diff --git a/node_modules/@tauri-apps/cli/src/lib.rs b/node_modules/@tauri-apps/cli/src/lib.rs new file mode 100644 index 0000000..213dd84 --- /dev/null +++ b/node_modules/@tauri-apps/cli/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use napi::{ + threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}, + Error, JsFunction, Result, Status, +}; + +#[napi_derive::napi] +pub fn run(args: Vec, bin_name: Option, callback: JsFunction) -> Result<()> { + let function: ThreadsafeFunction = callback + .create_threadsafe_function(0, |ctx| ctx.env.get_boolean(ctx.value).map(|v| vec![v]))?; + + // we need to run in a separate thread so Node.js (e.g. vue-cli-plugin-tauri) consumers + // can do work while `tauri dev` is running. + std::thread::spawn(move || match tauri_cli::try_run(args, bin_name) { + Ok(_) => function.call(Ok(true), ThreadsafeFunctionCallMode::Blocking), + Err(e) => function.call( + Err(Error::new(Status::GenericFailure, format!("{:#}", e))), + ThreadsafeFunctionCallMode::Blocking, + ), + }); + + Ok(()) +} + +#[napi_derive::napi] +pub fn log_error(error: String) { + log::error!("{}", error); +} diff --git a/node_modules/@tauri-apps/cli/tauri.js b/node_modules/@tauri-apps/cli/tauri.js new file mode 100644 index 0000000..8ee5b93 --- /dev/null +++ b/node_modules/@tauri-apps/cli/tauri.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const cli = require('./main') +const path = require('path') + +const [bin, script, ...args] = process.argv +const binStem = path.parse(bin).name.toLowerCase() + +// We want to make a helpful binary name for the underlying CLI helper, if we +// can successfully detect what command likely started the execution. +let binName + +// deno run -A --unstable --node-modules-dir npm:@tauri-apps/cli +if (bin === '@tauri-apps/cli') { + binName = '@tauri-apps/cli' +} +// Even if started by a package manager, the binary will be NodeJS. +// Some distribution still use "nodejs" as the binary name. +else if (binStem.match(/(nodejs|node|bun)\-?([0-9]*)*$/g)) { + const managerStem = process.env.npm_execpath + ? path.parse(process.env.npm_execpath).name.toLowerCase() + : null + if (managerStem) { + let manager + switch (managerStem) { + // Only supported package manager that has a different filename is npm. + case 'npm-cli': + manager = 'npm' + break + + // Yarn, pnpm, and bun have the same stem name as their bin. + // We assume all unknown package managers do as well. + default: + manager = managerStem + break + } + + binName = `${manager} run ${process.env.npm_lifecycle_event}` + } else { + // Assume running NodeJS if we didn't detect a manager from the env. + // We normalize the path to prevent the script's absolute path being used. + const scriptNormal = path.normalize(path.relative(process.cwd(), script)) + binName = `${binStem} ${scriptNormal}` + } +} else { + // We don't know what started it, assume it's already stripped. + args.unshift(bin) +} + +cli.run(args, binName).catch((err) => { + cli.logError(err.message) + process.exit(1) +}) diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png new file mode 100644 index 0000000..83630ba Binary files /dev/null and b/static/android-chrome-192x192.png differ diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png new file mode 100644 index 0000000..ffeda82 Binary files /dev/null and b/static/android-chrome-512x512.png differ diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..6098d72 Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/bullets/glitch_cannon_bullet.gif b/static/bullets/glitch_cannon_bullet.gif new file mode 100644 index 0000000..ef568e0 Binary files /dev/null and b/static/bullets/glitch_cannon_bullet.gif differ diff --git a/static/bullets/neon_blaster_bullet - Copy.gif b/static/bullets/neon_blaster_bullet - Copy.gif new file mode 100644 index 0000000..359e5b5 Binary files /dev/null and b/static/bullets/neon_blaster_bullet - Copy.gif differ diff --git a/static/bullets/neon_blaster_bullet.gif b/static/bullets/neon_blaster_bullet.gif new file mode 100644 index 0000000..b1aba06 Binary files /dev/null and b/static/bullets/neon_blaster_bullet.gif differ diff --git a/static/bullets/pew_pew_bullet.gif b/static/bullets/pew_pew_bullet.gif new file mode 100644 index 0000000..ef568e0 Binary files /dev/null and b/static/bullets/pew_pew_bullet.gif differ diff --git a/static/bullets/synth_rifle_bullet - Copy.gif b/static/bullets/synth_rifle_bullet - Copy.gif new file mode 100644 index 0000000..8ffbd6a Binary files /dev/null and b/static/bullets/synth_rifle_bullet - Copy.gif differ diff --git a/static/bullets/synth_rifle_bullet.gif b/static/bullets/synth_rifle_bullet.gif new file mode 100644 index 0000000..ef568e0 Binary files /dev/null and b/static/bullets/synth_rifle_bullet.gif differ diff --git a/static/classes/bandana_guy.png b/static/classes/bandana_guy.png new file mode 100644 index 0000000..7db23e6 Binary files /dev/null and b/static/classes/bandana_guy.png differ diff --git a/static/classes/cyber_sentinel.png b/static/classes/cyber_sentinel.png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/cyber_sentinel.png differ diff --git a/static/classes/dwarf.png b/static/classes/dwarf.png new file mode 100644 index 0000000..2697b99 Binary files /dev/null and b/static/classes/dwarf.png differ diff --git a/static/classes/green_alien.png b/static/classes/green_alien.png new file mode 100644 index 0000000..09b81c1 Binary files /dev/null and b/static/classes/green_alien.png differ diff --git a/static/classes/grid_glitch.png b/static/classes/grid_glitch.png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/grid_glitch.png differ diff --git a/static/classes/laser_lancer.png b/static/classes/laser_lancer.png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/laser_lancer.png differ diff --git a/static/classes/neon_nomad.png b/static/classes/neon_nomad.png new file mode 100644 index 0000000..875d99c Binary files /dev/null and b/static/classes/neon_nomad.png differ diff --git a/static/classes/propaneguy.png b/static/classes/propaneguy.png new file mode 100644 index 0000000..b1f54bc Binary files /dev/null and b/static/classes/propaneguy.png differ diff --git a/static/classes/silver_knight.png b/static/classes/silver_knight.png new file mode 100644 index 0000000..149180b Binary files /dev/null and b/static/classes/silver_knight.png differ diff --git a/static/classes/space_cadet - Copy (6).png b/static/classes/space_cadet - Copy (6).png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/space_cadet - Copy (6).png differ diff --git a/static/classes/space_cadet - Copy.png b/static/classes/space_cadet - Copy.png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/space_cadet - Copy.png differ diff --git a/static/classes/space_cadet.png b/static/classes/space_cadet.png new file mode 100644 index 0000000..1751242 Binary files /dev/null and b/static/classes/space_cadet.png differ diff --git a/static/classes/zombie.png b/static/classes/zombie.png new file mode 100644 index 0000000..de85ae7 Binary files /dev/null and b/static/classes/zombie.png differ diff --git a/static/enemies/bluedino.png b/static/enemies/bluedino.png new file mode 100644 index 0000000..1358867 Binary files /dev/null and b/static/enemies/bluedino.png differ diff --git a/static/enemies/greendino.png b/static/enemies/greendino.png new file mode 100644 index 0000000..29aafee Binary files /dev/null and b/static/enemies/greendino.png differ diff --git a/static/enemies/greengoblin.png b/static/enemies/greengoblin.png new file mode 100644 index 0000000..08bac78 Binary files /dev/null and b/static/enemies/greengoblin.png differ diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000..b0e0bae Binary files /dev/null and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000..a9278ed Binary files /dev/null and b/static/favicon-32x32.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..4e71c0e Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/hats/cowboy_hat.png b/static/hats/cowboy_hat.png new file mode 100644 index 0000000..8dc26ee Binary files /dev/null and b/static/hats/cowboy_hat.png differ diff --git a/static/hats/crown.png b/static/hats/crown.png new file mode 100644 index 0000000..4365837 Binary files /dev/null and b/static/hats/crown.png differ diff --git a/static/hats/wizard_hat.png b/static/hats/wizard_hat.png new file mode 100644 index 0000000..c7d84e9 Binary files /dev/null and b/static/hats/wizard_hat.png differ diff --git a/static/js/cursor.js b/static/js/cursor.js new file mode 100644 index 0000000..40d4113 --- /dev/null +++ b/static/js/cursor.js @@ -0,0 +1,38 @@ +// Add this to your game.js file or create a new cursor.js file + +document.addEventListener('DOMContentLoaded', () => { + const cursor = document.createElement('div'); + cursor.classList.add('custom-cursor'); + cursor.innerHTML = '
'; + document.body.appendChild(cursor); + + document.addEventListener('mousemove', (e) => { + cursor.style.left = e.clientX + 'px'; + cursor.style.top = e.clientY + 'px'; + }); + + document.addEventListener('mousedown', () => { + cursor.classList.add('clicking'); + }); + + document.addEventListener('mouseup', () => { + cursor.classList.remove('clicking'); + }); + + // Optional: Add a subtle trailing effect + let trailDot = null; + setInterval(() => { + if (trailDot) { + trailDot.remove(); + } + trailDot = cursor.querySelector('.cursor-dot').cloneNode(true); + trailDot.style.transition = 'all 0.15s linear'; + trailDot.style.opacity = '0.5'; + cursor.appendChild(trailDot); + setTimeout(() => { + if (trailDot) { + trailDot.style.opacity = '0'; + } + }, 50); + }, 100); +}); \ No newline at end of file diff --git a/static/js/draggable.js b/static/js/draggable.js new file mode 100644 index 0000000..66600ad --- /dev/null +++ b/static/js/draggable.js @@ -0,0 +1,108 @@ +function makeDraggable(element) { + let isDragging = false; + let startX, startY, initialLeft, initialTop; + + // Check if the device is mobile + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + if (!isMobile) { + element.addEventListener("mousedown", dragStart, { passive: false }); + document.addEventListener("mousemove", drag, { passive: false }); + document.addEventListener("mouseup", dragEnd); + } + + // For mobile devices, we'll only add touch events if we want to allow dragging + // If you want to completely disable dragging on mobile, you can remove these lines + element.addEventListener("touchstart", dragStart, { passive: true }); + document.addEventListener("touchmove", drag, { passive: true }); + document.addEventListener("touchend", dragEnd); + + function dragStart(e) { + if (e.target.closest('.close-notification')) return; + + if (isMobile) { + // On mobile, only start dragging if it's not a scrollable element + const isScrollable = e.target.closest('.widget-content') !== null; + if (isScrollable) return; + } + + isDragging = true; + + if (e.type === "touchstart") { + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + } else { + startX = e.clientX; + startY = e.clientY; + } + + initialLeft = element.offsetLeft; + initialTop = element.offsetTop; + + element.style.position = 'absolute'; + element.style.zIndex = 1000; + } + + function drag(e) { + if (!isDragging) return; + + if (isMobile) { + // Prevent default only if we're actually dragging + e.preventDefault(); + } + + let clientX, clientY; + if (e.type === "touchmove") { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { + clientX = e.clientX; + clientY = e.clientY; + } + + const deltaX = clientX - startX; + const deltaY = clientY - startY; + + element.style.left = initialLeft + deltaX + 'px'; + element.style.top = initialTop + deltaY + 'px'; + } + + function dragEnd(e) { + isDragging = false; + element.style.zIndex = ''; + } +} + + + // Function to apply draggable functionality to an element + function applyDraggable(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.classList.add('draggable'); + makeDraggable(element); + } + } + + // Apply draggable functionality to widgets and notification popups + function initDraggableElements() { + applyDraggable('uiWidget'); + applyDraggable('infoWidget'); + applyDraggable('classWidget'); + applyDraggable('shopWidget'); + + // For notifications, we need to apply it dynamically when they're created + const originalShowNotification = notificationSystem.showNotification; + notificationSystem.showNotification = function(message) { + originalShowNotification.call(this, message); + const notifications = document.querySelectorAll('.custom-notification'); + notifications.forEach(notification => { + notification.classList.add('draggable'); + makeDraggable(notification); + }); + }; + } + + // Call this function after your DOM is loaded + document.addEventListener('DOMContentLoaded', initDraggableElements); + + \ No newline at end of file diff --git a/static/js/game.js b/static/js/game.js new file mode 100644 index 0000000..4e803e1 --- /dev/null +++ b/static/js/game.js @@ -0,0 +1,3576 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const healthElement = document.getElementById('health'); +const startMessage = document.getElementById('startMessage'); +//const weaponSelector = document.getElementById('weaponSelector'); +const playButton = document.getElementById('playButton'); +const quitButton = document.getElementById('quitButton'); +const imageCache = new Map(); +const bulletImageCache = new Map(); +const enemyImageCache = new Map(); + + +canvas.width = window.innerWidth; +canvas.height = window.innerHeight; + +const MAP_WIDTH = 3000; +const MAP_HEIGHT = 2000; + +let audioContext, analyser, source; +let audioElement; +let player, camera; +let floatingTexts = []; +let otherPlayers = {}; +let serverEnemies = []; +let serverBullets = []; +let particles = []; +let score = 0, health = 100; +let gameStarted = false; +let joystick = { active: false, startX: 0, startY: 0, endX: 0, endY: 0 }; +let isDebugging = document.getElementById('debugCheckbox').checked; +let socket; +let currentSongDuration = 180; // Default to 3 minutes +let backgroundCanvas, backgroundCtx; +let uiButton, uiWidget, menuButton; +let isWidgetVisible = false; +let audioData = new Uint8Array(128); +let currentUser = null; +let minimapCanvas, minimapCtx, minimapState = 0; +let playerLevel = 1; +let playerExperience = 0; +let playerMaxExperience = 100; +let isPlayerPaused = false; +let openInterfaces = { + menu: false, + levelUp: false +}; +let selectedClassId = null; +const infoButton = document.getElementById('infoButton'); +const infoWidget = document.getElementById('infoWidget'); +let isInfoWidgetVisible = false; +let playerSynthCoins = 0; +let synthCoinsCounter; +let currentCoinFloatingText = null; +let shopButton; +let shopWidget; +let isShopVisible = false; +let playerWeapons = []; +let wasPausedBeforeHidden = false; +let isAutoFireEnabled = false; +let shootingJoystick = { + active: false, + startX: 0, + startY: 0, + endX: 0, + endY: 0, + outerRadius: 50, + innerRadius: 0, + maxInnerRadius: 40, + shootThreshold: 0.5, + growthRate: 200, // pixels per second +}; +let lastUpdateTime = Date.now(); +let impactEffects = []; + + + + + +class NotificationSystem { + constructor() { + this.notificationContainer = null; + this.createNotificationContainer(); + } + + createNotificationContainer() { + this.notificationContainer = document.createElement('div'); + this.notificationContainer.id = 'notificationContainer'; + document.body.appendChild(this.notificationContainer); + } + + showNotification(message) { + const notificationElement = document.createElement('div'); + notificationElement.className = 'custom-notification draggable'; + notificationElement.innerHTML = ` +
+

${message}

+ +
+ `; + + const closeButton = notificationElement.querySelector('.close-notification'); + closeButton.addEventListener('click', () => this.closeNotification(notificationElement)); + + this.notificationContainer.appendChild(notificationElement); + setTimeout(() => notificationElement.classList.add('show'), 10); + + makeDraggable(notificationElement); + } + + + closeNotification(notificationElement) { + notificationElement.classList.remove('show'); + setTimeout(() => notificationElement.remove(), 300); + } +} + +// Create a global instance of the NotificationSystem +const notificationSystem = new NotificationSystem(); + +function initShop() { + let shopButton = document.getElementById('shopButton'); + if (!shopButton) { + shopButton = document.createElement('button'); + shopButton.id = 'shopButton'; + shopButton.textContent = 'Shop'; + shopButton.classList.add('game-button'); + shopButton.style.display = 'none'; // Hide by default + shopButton.addEventListener('click', toggleShop); + document.body.appendChild(shopButton); + } + + let shopWidget = document.getElementById('shopWidget'); + if (!shopWidget) { + shopWidget = document.createElement('div'); + shopWidget.id = 'shopWidget'; + shopWidget.classList.add('hidden'); + shopWidget.innerHTML = ` +
+ +
+
+
+
+
+
+

+
+ +
+
+
+
+
+ + `; + document.body.appendChild(shopWidget); + } + + shopWidget.querySelector('.close-button').addEventListener('click', hideShop); + + // Add event listener for the shop tab button + const shopTabButton = shopWidget.querySelector('.tab-button[data-tab="shop"]'); + shopTabButton.addEventListener('click', () => showShopTab('shop')); +} + +function showShopTab(tabName) { + const shopWidget = document.getElementById('shopWidget'); + const tabContents = shopWidget.querySelectorAll('.tab-content'); + const tabButtons = shopWidget.querySelectorAll('.tab-button'); + + tabContents.forEach(content => content.classList.remove('active')); + tabButtons.forEach(button => button.classList.remove('active')); + + const selectedTab = shopWidget.querySelector(`#${tabName}`); + const selectedButton = shopWidget.querySelector(`.tab-button[data-tab="${tabName}"]`); + + if (selectedTab && selectedButton) { + selectedTab.classList.add('active'); + selectedButton.classList.add('active'); + } +} + + +function showShopButton() { + const shopButton = document.getElementById('shopButton'); + if (shopButton) { + shopButton.style.display = 'block'; + } else { + console.error('Shop button not found in the DOM'); + } +} + + + +function toggleShop() { + const shopWidget = document.getElementById('shopWidget'); + if (shopWidget.classList.contains('visible')) { + hideShop(); + } else { + showShop(); + } +} + +// Add this function to show the shop +function showShop() { + const shopWidget = document.getElementById('shopWidget'); + shopWidget.classList.remove('hidden'); + shopWidget.classList.add('visible'); + + // Force a refresh of the shop items + fetchShopItems().then(() => { + forceShowShopContent(); + debugShopWidget(); + }); + + pausePlayer(); +} + +function forceShowShopContent() { + const shopWidget = document.getElementById('shopWidget'); + const shopContent = shopWidget.querySelector('#shop'); + const shopTab = shopWidget.querySelector('.tab-button[data-tab="shop"]'); + + // Hide all tab contents + const allTabContents = shopWidget.querySelectorAll('.tab-content'); + allTabContents.forEach(content => content.style.display = 'none'); + + // Deactivate all tabs + const allTabs = shopWidget.querySelectorAll('.tab-button'); + allTabs.forEach(tab => tab.classList.remove('active')); + + // Show shop content and activate shop tab + if (shopContent) { + shopContent.style.display = 'block'; + } + if (shopTab) { + shopTab.classList.add('active'); + } +} + + + +// Add this function to hide the shop +function hideShop() { + const shopWidget = document.getElementById('shopWidget'); + shopWidget.classList.remove('visible'); + shopWidget.classList.add('hidden'); + unpausePlayer(); +} + +function fetchShopItems() { + if (!player || !player.playerClass || !player.playerClass._id) { + console.error('Player or player class not initialized'); + return Promise.resolve(); + } + + return fetch('/get-shop-items', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ player_class: player.playerClass._id }), + }) + .then(response => response.json()) + .then(weapons => { + displayShopItems(weapons); + }) + .catch(error => { + console.error('Error fetching shop items:', error); + }); +} + + + +// Add this function to display shop items +function displayShopItems(weapons) { + const weaponSelector = document.getElementById('weaponSelector'); + if (!weaponSelector) { + console.error('Weapon selector not found'); + return; + } + weaponSelector.innerHTML = ''; // Clear existing content + + weapons.forEach(weapon => { + const weaponOption = document.createElement('div'); + weaponOption.classList.add('weapon-option'); + weaponOption.innerHTML = `${weapon.name}`; + weaponOption.addEventListener('click', () => selectWeapon(weapon, weaponOption)); + weaponSelector.appendChild(weaponOption); + }); + + if (weapons.length > 0) { + selectWeapon(weapons[0], weaponSelector.firstChild); + } else { + debugLog('No weapons available in the shop'); + } +} + + +function selectWeapon(weapon, selectedElement) { + const weaponOptions = document.querySelectorAll('.weapon-option'); + weaponOptions.forEach(option => option.classList.remove('selected')); + selectedElement.classList.add('selected'); + + const weaponImage = document.getElementById('weaponImage'); + const weaponName = document.getElementById('weaponName'); + const weaponStats = document.getElementById('weaponStats'); + const buyButton = document.getElementById('buyWeaponButton'); + + if (weaponImage && weaponName && weaponStats && buyButton) { + weaponImage.innerHTML = `${weapon.name}`; + weaponName.textContent = weapon.name; + weaponStats.innerHTML = ` +

Damage: ${weapon.base_attributes.damage}

+

Fire Rate: ${weapon.base_attributes.fire_rate}

+

Bullet Speed: ${weapon.base_attributes.bullet_speed}

+

Bullet Size: ${weapon.base_attributes.bullet_size}

+

Price: ${weapon.price} Synth Coins

+ `; + buyButton.onclick = () => buyWeapon(weapon); + } else { + console.error('One or more weapon info elements not found'); + } +} + + + +// Add this function to create a shop item element +function createShopItemElement(weapon) { + const itemElement = document.createElement('div'); + itemElement.classList.add('shop-item'); + itemElement.innerHTML = ` + ${weapon.name} +

${weapon.name}

+

Price: ${weapon.price} Synth Coins

+ + `; + + const buyButton = itemElement.querySelector('.buy-button'); + buyButton.addEventListener('click', () => buyWeapon(weapon)); + + return itemElement; +} + +// Add this function to handle weapon purchase +function buyWeapon(weapon) { + fetch('/buy-weapon', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + player_id: socket.id, + weapon_id: weapon._id, + }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + debugLog('Weapon purchase successful:', data); + player.synthCoins = data.updatedPlayerData.synth_coins; + player.weapons = data.updatedPlayerData.weapons; + player.loadWeaponImages(); + player.weaponAngles = player.weapons.map(() => 0); + player.lastShootTimes = player.weapons.map(() => 0); + updateSynthCoinsDisplay(); + notificationSystem.showNotification('Weapon purchased successfully!'); + fetchShopItems(); // Refresh shop items + } else { + notificationSystem.showNotification(data.message); + } + }) + .catch(error => { + console.error('Error buying weapon:', error); + notificationSystem.showNotification('An error occurred while purchasing the weapon: ' + error.message); + }); +} + + + + + +function loadAndCacheImage(src) { + if (imageCache.has(src)) { + return imageCache.get(src); + } + const img = new Image(); + img.src = src; + img.onerror = () => { + console.error(`Failed to load image: ${src}`); + imageCache.set(src, null); + }; + imageCache.set(src, img); + return img; +} + + +function preloadBulletImage(src) { + if (bulletImageCache.has(src)) { + return bulletImageCache.get(src); + } + const img = new Image(); + img.src = src; + bulletImageCache.set(src, img); + return img; +} + +function preloadEnemyImages() { + fetch('/get-enemy-images') + .then(response => response.json()) + .then(images => { + images.forEach(imageSrc => { + if (!enemyImageCache.has(imageSrc)) { + const img = new Image(); + img.src = imageSrc; + enemyImageCache.set(imageSrc, img); + } + }); + }) + .catch(error => console.error('Error preloading enemy images:', error)); +} + + +class Camera { + constructor(width, height) { + this.x = 0; + this.y = 0; + this.width = width; + this.height = height; + } + + follow(player) { + this.x = Math.floor(player.x - this.width / 2); + this.y = Math.floor(player.y - this.height / 2); + this.x = Math.max(0, Math.min(this.x, MAP_WIDTH - this.width)); + this.y = Math.max(0, Math.min(this.y, MAP_HEIGHT - this.height)); + } + +} + +// Add this new class for managing trails +class Trail { + constructor(maxLength = 10) { + this.positions = []; + this.maxLength = maxLength; + } + + update(x, y) { + this.positions.unshift({x, y}); + if (this.positions.length > this.maxLength) { + this.positions.pop(); + } + } + + draw(ctx, color) { + if (!ctx) { + console.error('Canvas context is undefined in Trail.draw'); + return; + } + for (let i = 0; i < this.positions.length; i++) { + const alpha = 1 - (i / this.positions.length); + ctx.beginPath(); + ctx.arc(this.positions[i].x, this.positions[i].y, 5, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`; + ctx.fill(); + ctx.closePath(); + } + } +} + +class Skin { + constructor(type, color, shape) { + this.type = type; + this.color = color; + this.shape = shape; + this.animationPhase = 0; + } + + update() { + this.animationPhase = (this.animationPhase + 0.1) % (2 * Math.PI); + } + + draw(ctx, x, y, radius) { + ctx.save(); + ctx.translate(x, y); + + switch (this.shape) { + case 'circle': + this.drawCircle(ctx, radius); + break; + case 'triangle': + this.drawTriangle(ctx, radius); + break; + case 'square': + this.drawSquare(ctx, radius); + break; + default: + this.drawCircle(ctx, radius); + } + + ctx.restore(); + } + + drawCircle(ctx, radius) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + + // Add pulsating effect + const pulseRadius = radius * (1 + 0.1 * Math.sin(this.animationPhase)); + ctx.beginPath(); + ctx.arc(0, 0, pulseRadius, 0, Math.PI * 2); + ctx.strokeStyle = this.color; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.closePath(); + } + + drawTriangle(ctx, radius) { + ctx.beginPath(); + ctx.moveTo(0, -radius); + ctx.lineTo(-radius * Math.sqrt(3) / 2, radius / 2); + ctx.lineTo(radius * Math.sqrt(3) / 2, radius / 2); + ctx.closePath(); + ctx.fillStyle = this.color; + ctx.fill(); + + // Add rotating effect + ctx.save(); + ctx.rotate(this.animationPhase); + ctx.beginPath(); + ctx.moveTo(0, -radius * 1.2); + ctx.lineTo(-radius * Math.sqrt(3) / 2 * 1.2, radius / 2 * 1.2); + ctx.lineTo(radius * Math.sqrt(3) / 2 * 1.2, radius / 2 * 1.2); + ctx.closePath(); + ctx.strokeStyle = this.color; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.restore(); + } + + drawSquare(ctx, radius) { + ctx.beginPath(); + ctx.rect(-radius, -radius, radius * 2, radius * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + + // Add spinning effect + ctx.save(); + ctx.rotate(this.animationPhase); + ctx.beginPath(); + ctx.rect(-radius * 1.2, -radius * 1.2, radius * 2.4, radius * 2.4); + ctx.strokeStyle = this.color; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.restore(); + } +} + + +// Update the Player class +class Player { + constructor(x, y, name, skinData, weapons, playerClass, ctx) { + this.x = x; + this.y = y; + this.name = name; + this.radius = 20; + this.maxSpeed = playerClass?.base_attributes?.speed || 5; // Default to 5 if undefined + this.velocityX = 0; + this.velocityY = 0; + this.keys = { up: false, down: false, left: false, right: false }; + this.trail = new Trail(); + this.setSkin(skinData); + this.weapons = Array.isArray(weapons) ? weapons : (weapons ? [weapons] : []); + this.lastShootTimes = this.weapons.map(() => 0); + this.weaponImages = new Map(); + this.currentWeaponIndex = 0; + this.weaponAngle = 0; + this.weaponAngles = this.weapons.map(() => 0); + this.orbitRadius = 40; // Distance of weapons from player center + this.playerClass = playerClass || {}; // Default to empty object if undefined + this.playerClassImage = playerClass ? loadAndCacheImage(playerClass.image_source) : null; + this.health = playerClass?.base_attributes?.health || 100; // Default to 100 if undefined + this.damageMultiplier = playerClass?.base_attributes?.damage_multiplier || 1; // Default to 1 if undefined + this.level = 1; + this.lastShootTime = 0; + this.ctx = ctx; + this.experience = 0; + this.maxExperience = 100; + this.isPaused = false; + this.synthCoins = 0; + this.maxHealth = playerClass.base_attributes.health; + this.currentHealth = this.maxHealth; + this.healthRegen = playerClass.base_attributes.health_regen || 1; + this.isShooting = false; + this.mouseX = 0; + this.mouseY = 0; + this.autoFireEnabled = false; + + } + + loadWeaponImages() { + this.weapons.forEach(weapon => { + if (weapon && weapon.image_source) { + this.loadWeaponImage(weapon.image_source); + } + }); + } + + loadWeaponImage(imageSrc) { + if (!this.weaponImages.has(imageSrc)) { + const img = new Image(); + img.src = imageSrc; + img.onload = () => { + this.weaponImages.set(imageSrc, img); + }; + img.onerror = () => { + console.error(`Failed to load weapon image: ${imageSrc}`); + }; + } + } + + + setSkin(skinData) { + this.skin = skinData; + if (this.skin && this.skin.hat) { + this.loadHatImage(); // Call this method to load the hat image + } + } + + getCurrentWeapon() { + return this.weapons[this.currentWeaponIndex]; + } + + upgradeWeapon(attribute, value) { + const currentWeapon = this.getCurrentWeapon(); + if (currentWeapon && currentWeapon.base_attributes) { + if (attribute in currentWeapon.base_attributes) { + currentWeapon.base_attributes[attribute] += value; + debugLog(`Upgraded ${attribute} to ${currentWeapon.base_attributes[attribute]}`); + } else { + console.error(`Attribute ${attribute} not found in weapon attributes`); + } + } else { + console.error('Current weapon or base attributes not found'); + } + } + + loadHatImage() { + if (this.skin.hat && this.skin.hat.value) { + this.hatImage = new Image(); + this.hatImage.src = this.skin.hat.value; + this.hatImage.onload = () => { + debugLog(`Hat image loaded for player ${this.name}`); + }; + } + } + + updateHealth() { + // Health update logic will be handled by the server + // This method is here for potential client-side predictions or interpolations + } + + + + update() { + if (this.keys.up) this.velocityY -= 0.5; + if (this.keys.down) this.velocityY += 0.5; + if (this.keys.left) this.velocityX -= 0.5; + if (this.keys.right) this.velocityX += 0.5; + + this.velocityX *= 0.95; + this.velocityY *= 0.95; + + const speed = Math.sqrt(this.velocityX * this.velocityX + this.velocityY * this.velocityY); + if (speed > this.maxSpeed) { + const ratio = this.maxSpeed / speed; + this.velocityX *= ratio; + this.velocityY *= ratio; + } + + this.x += this.velocityX; + this.y += this.velocityY; + + this.x = Math.max(this.radius, Math.min(MAP_WIDTH - this.radius, this.x)); + this.y = Math.max(this.radius, Math.min(MAP_HEIGHT - this.radius, this.y)); + + this.trail.update(this.x, this.y); + this.updateWeaponAngle(); + this.updateHealth(); + + } + + updateWeaponAngle(nearestEnemy) { + if (this.autoFireEnabled && nearestEnemy) { + const targetAngle = Math.atan2(nearestEnemy.y - this.y, nearestEnemy.x - this.x); + this.weaponAngles = this.weaponAngles.map((angle, index) => { + const baseAngle = (index / this.weapons.length) * Math.PI * 2; + return lerpAngle(angle, targetAngle + baseAngle, 0.1); + }); + } + } + + findNearestEnemy() { + return findNearestEnemy(); + } + + draw() { + if (!this.ctx) { + console.error('Canvas context is undefined in Player.draw for player:', this.name); + return; + } + this.trail.draw(this.ctx, [0, 255, 255]); + + // Apply color effect if any + if (this.skin && this.skin.color && this.skin.color.effect === 'glow') { + this.ctx.beginPath(); + this.ctx.arc(this.x, this.y, this.radius + 5, 0, Math.PI * 2); + this.ctx.fillStyle = `${this.skin.color.value}33`; + this.ctx.fill(); + this.ctx.closePath(); + } + + // Draw player class image + if (this.playerClassImage && this.playerClassImage.complete) { + this.ctx.save(); + this.ctx.translate(this.x, this.y); + + // Apply glow effect to the class image if present + if (this.skin && this.skin.color && this.skin.color.effect === 'glow') { + this.ctx.shadowColor = this.skin.color.value; + this.ctx.shadowBlur = 10; + } + + this.ctx.drawImage(this.playerClassImage, -this.radius, -this.radius, this.radius * 2, this.radius * 2); + this.ctx.restore(); + } + + // Draw weapon + if (this.weaponImage && this.weaponImage.complete) { + this.ctx.drawImage(this.weaponImage, this.x + this.radius, this.y - this.radius / 2, this.radius, this.radius); + } + + // Draw hat if present + this.drawHat(); + + // Set font for all text + this.ctx.font = '12px Orbitron'; + this.ctx.textAlign = 'center'; + this.ctx.fillStyle = 'white'; + + // Draw player name + this.ctx.fillText(this.name, this.x, this.y - this.radius - 5); + + // Draw player level + this.ctx.fillText(`Lvl ${this.level}`, this.x, this.y - this.radius - 20); + + // Draw paused indicator if the player is paused + if (this.isPaused) { + this.ctx.font = '16px Orbitron'; + this.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; + this.ctx.fillText('PAUSED', this.x, this.y - this.radius - 30); + } + + this.drawWeapons(); + this.drawHealthBar(); + } + + drawHealthBar() { + const barWidth = this.radius * 2; + const barHeight = 5; + const x = this.x - this.radius; + const y = this.y + this.radius + 5; + + // Draw background + this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + this.ctx.fillRect(x, y, barWidth, barHeight); + + // Draw health + const healthPercentage = this.currentHealth / this.maxHealth; + this.ctx.fillStyle = `rgb(${255 - healthPercentage * 255}, ${healthPercentage * 255}, 0)`; + this.ctx.fillRect(x, y, barWidth * healthPercentage, barHeight); + } + + + addWeapon(weapon) { + this.weapons.push(weapon); + this.weaponAngles.push(0); + this.lastShootTimes.push(0); + if (weapon && weapon.image_source) { + this.loadWeaponImage(weapon.image_source); + } + } + + updateWeaponPositions() { + const angleStep = (2 * Math.PI) / this.weapons.length; + this.weaponAngles = this.weapons.map((_, index) => index * angleStep); + } + + + + drawWeapons() { + this.weapons.forEach((weapon, index) => { + if (weapon && weapon.image_source) { + const angle = this.weaponAngles[index]; + const weaponX = this.x + Math.cos(angle) * this.orbitRadius; + const weaponY = this.y + Math.sin(angle) * this.orbitRadius; + + const img = this.weaponImages.get(weapon.image_source); + if (img && img.complete) { + this.ctx.save(); + this.ctx.translate(weaponX, weaponY); + this.ctx.rotate(angle); + this.ctx.drawImage(img, -this.radius / 2, -this.radius / 2, this.radius, this.radius); + this.ctx.restore(); + } + } + }); + } + + setWeaponAngle(angle) { + this.weaponAngle = angle; + this.weaponAngles = this.weapons.map(() => angle); + } + + drawHat() { + if (this.hatImage && this.hatImage.complete) { + const hatSize = this.radius * 1.5; + const hatOffsetY = this.radius * 0.9; // Adjust this value to move the hat up or down + this.ctx.drawImage( + this.hatImage, + this.x - hatSize / 2, + this.y - this.radius - hatOffsetY, // Changed this line + hatSize, + hatSize + ); + } else if (this.skin && this.skin.hat) { + debugLog(`Hat image not loaded for player ${this.name}`); + } + } + + updateWeaponAngleToMouse() { + if (this.mouseX && this.mouseY) { + const dx = this.mouseX - this.x; + const dy = this.mouseY - this.y; + const angle = Math.atan2(dy, dx); + this.setWeaponAngle(angle); + + // Send weapon angle update to server + socket.emit('player_update', { + x: this.x, + y: this.y, + weapon_angles: this.weaponAngles, + auto_fire_enabled: isAutoFireEnabled + }); + } + } + + + shoot(currentTime) { + if (this.isPaused) return; + + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + this.weapons.forEach((weapon, index) => { + if (!weapon || !weapon.base_attributes) { + console.error('Invalid weapon structure:', weapon); + return; + } + + const fireRate = weapon.base_attributes.fire_rate || 1; + if (currentTime - this.lastShootTimes[index] > 1000 / fireRate) { + let shootAngle; + if (isAutoFireEnabled) { + const nearestEnemy = findNearestEnemy(); + shootAngle = nearestEnemy ? Math.atan2(nearestEnemy.y - this.y, nearestEnemy.x - this.x) : this.weaponAngle; + } else { + shootAngle = this.weaponAngle; + } + + const weaponX = this.x + Math.cos(this.weaponAngles[index]) * this.orbitRadius; + const weaponY = this.y + Math.sin(this.weaponAngles[index]) * this.orbitRadius; + + if ((isMobile && this.isShooting) || (!isMobile && (this.isShooting || isAutoFireEnabled))) { + socket.emit('player_shoot', { angle: shootAngle, weaponX, weaponY, weaponIndex: index }); + this.lastShootTimes[index] = currentTime; + } + } + }); + } +} + +// Add this function to draw the UI +function drawUI() { +} + + + +// Add a new Enemy class +class Enemy { + constructor(data) { + this.id = data.id; + this.name = data.name; + this.x = data.x; + this.y = data.y; + this.image = data.image; + this.attributes = data.attributes; + this.difficulty = data.difficulty; + this.currentHealth = data.current_health; + this.maxHealth = data.attributes.health; + this.experience = data.experience; + this.trail = new Trail(); + this.loadImage(); + } + + loadImage() { + if (!enemyImageCache.has(this.image)) { + const img = new Image(); + img.src = this.image; + enemyImageCache.set(this.image, img); + } + } + + update() { + this.trail.update(this.x, this.y); + } + + draw(ctx) { + this.trail.draw(ctx, [255, 0, 0]); // Red trail for enemies + const img = enemyImageCache.get(this.image); + if (img && img.complete) { + ctx.drawImage(img, this.x - 15, this.y - 15, 30, 30); // Assuming enemy size is 30x30 + } else { + // Fallback to a colored circle if image is not loaded + ctx.beginPath(); + ctx.arc(this.x, this.y, 15, 0, Math.PI * 2); + ctx.fillStyle = 'red'; + ctx.fill(); + ctx.closePath(); + } + + // Draw health bar + const healthBarWidth = 30; + const healthBarHeight = 4; + const healthPercentage = this.currentHealth / this.maxHealth; + ctx.fillStyle = 'red'; + ctx.fillRect(this.x - healthBarWidth / 2, this.y - 20, healthBarWidth, healthBarHeight); + ctx.fillStyle = 'green'; + ctx.fillRect(this.x - healthBarWidth / 2, this.y - 20, healthBarWidth * healthPercentage, healthBarHeight); + } +} + + +function init() { + camera = new Camera(canvas.width, canvas.height); + initAudio(); + initDraggableElements(); + + const autoFireCheckbox = document.getElementById('autoFireCheckbox'); + autoFireCheckbox.addEventListener('change', (e) => { + isAutoFireEnabled = e.target.checked; + if (isDebugging) { + console.log('[DEBUG] Auto Fire ' + (isAutoFireEnabled ? 'enabled' : 'disabled')); + } + }); + + + // Set up background canvas + backgroundCanvas = document.createElement('canvas'); + backgroundCanvas.width = canvas.width; + backgroundCanvas.height = canvas.height; + backgroundCtx = backgroundCanvas.getContext('2d'); + backgroundCtx.fillStyle = 'rgb(18, 4, 88)'; + backgroundCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height); + + uiButton = document.getElementById('uiButton'); + uiWidget = document.getElementById('uiWidget'); + menuButton = document.getElementById('menuButton'); + + if (uiButton) uiButton.addEventListener('click', toggleWidget); + if (menuButton) menuButton.addEventListener('click', toggleWidget); + + + uiButton.addEventListener('click', toggleWidget); + menuButton.addEventListener('click', toggleWidget); + + minimapCanvas = document.getElementById('minimap'); + minimapCtx = minimapCanvas.getContext('2d'); + + const toggleMinimapButton = document.getElementById('toggle-minimap'); + toggleMinimapButton.addEventListener('click', toggleMinimap); + + const debugCheckbox = document.getElementById('debugCheckbox'); + debugCheckbox.addEventListener('change', (e) => { + isDebugging = e.target.checked; + if (isDebugging) { + console.log('[DEBUG] Debugging mode enabled'); + } else { + console.log('Debugging mode disabled'); + } + }); + + canvas.addEventListener('mousedown', (e) => { + if (!isAutoFireEnabled && player) { + player.isShooting = true; + } + }); + + canvas.addEventListener('mouseup', (e) => { + if (!isAutoFireEnabled && player) { + player.isShooting = false; + } + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isAutoFireEnabled && player) { + updatePlayerAim(e.clientX, e.clientY); + } + }); + + + + const tabButtons = document.querySelectorAll('.tab-button'); + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const tabName = button.getAttribute('data-tab'); + if (tabName === 'close') { + hideWidget(); + } else { + showTab(tabName); + } + }); + }); + + const closeButtons = document.querySelectorAll('.close-button'); + closeButtons.forEach(button => { + button.addEventListener('click', () => { + hideClassSelector(); + hideWidget(); + hideInfoWidget(); + }); + }); + + // Focus + // document.addEventListener('click', handleOutsideClick); + + const classSelectorButton = document.getElementById('classSelectorButton'); + if (classSelectorButton) classSelectorButton.addEventListener('click', showClassSelector); + classSelectorButton.addEventListener('click', showClassSelector); + + checkSession(); + initShop(); + + + // Add event listeners for auth buttons + document.getElementById('signupButton').addEventListener('click', showSignupForm); + document.getElementById('loginButton').addEventListener('click', showLoginForm); + document.getElementById('logoutButton').addEventListener('click', logout); + document.getElementById('submitSignup').addEventListener('click', signup); + document.getElementById('submitLogin').addEventListener('click', login); + + // Add input validation listeners + document.getElementById('signupUsername').addEventListener('input', validateSignupInput); + document.getElementById('signupEmail').addEventListener('input', validateSignupInput); + document.getElementById('signupPassword').addEventListener('input', validateSignupInput); + + infoButton.addEventListener('click', toggleInfoWidget); + + const classWidgetTabs = document.querySelectorAll('#classWidget .widget-tabs .tab-button'); + classWidgetTabs.forEach(tab => { + tab.addEventListener('click', function() { + const tabName = this.getAttribute('data-tab'); + showTab(tabName, 'classWidget'); + }); + }); + + + const infoTabButtons = infoWidget.querySelectorAll('.tab-button'); + infoTabButtons.forEach(button => { + button.addEventListener('click', () => { + const tabName = button.getAttribute('data-tab'); + showInfoTab(tabName); + }); + }); + + infoWidget.querySelector('.close-button').addEventListener('click', hideInfoWidget); + stopPropagationOnUIElements(); +} + +function debugLog(...args) { + if (isDebugging) { + console.log('[DEBUG]', ...args); + } +} + + + + +function updateSynthCoinsDisplay() { + if (!gameStarted) return; + + const counter = document.getElementById('synthCoinsCounter'); + if (!counter) { + const newCounter = document.createElement('canvas'); + newCounter.id = 'synthCoinsCounter'; + document.body.appendChild(newCounter); + } + + const counterCanvas = document.getElementById('synthCoinsCounter'); + const counterCtx = counterCanvas.getContext('2d'); + + const text = `Synth Coins: ${player ? player.synthCoins : 0}`; + counterCtx.font = '16px Orbitron'; + const textWidth = counterCtx.measureText(text).width; + + counterCanvas.width = textWidth + 20; + counterCanvas.height = 30; + + counterCtx.fillStyle = '#FFD700'; + counterCtx.font = '16px Orbitron'; + counterCtx.textAlign = 'left'; + counterCtx.textBaseline = 'middle'; + + counterCtx.shadowColor = '#FFD700'; + counterCtx.shadowBlur = 5; + counterCtx.fillText(text, 10, counterCanvas.height / 2); + + counterCtx.shadowBlur = 0; +} + + + + +function showClassSelector() { + const classWidget = document.getElementById('classWidget'); + classWidget.classList.add('visible'); + selectClassesTab(); + fetchAndDisplayClasses(); +} + +function selectClassesTab() { + // Select the "Classes" tab + const classesTab = document.querySelector('.widget-tabs .tab-button[data-tab="classes"]'); + if (classesTab) { + classesTab.classList.add('active'); // Add active class instead of simulating click + } + + // Show the "Classes" content + showTab('classes'); // Use the existing showTab function +} + + +function hideClassSelector() { + const classWidget = document.getElementById('classWidget'); + classWidget.classList.remove('visible'); +} + +function fetchAndDisplayClasses() { + fetch('/get-available-classes') + .then(response => response.json()) + .then(classes => { + // Sort classes: unlocked first, then locked + classes.sort((a, b) => { + if (a.unlocked_by_default === b.unlocked_by_default) return 0; + return a.unlocked_by_default ? -1 : 1; + }); + + const classSelector = document.getElementById('classSelector'); + classSelector.innerHTML = ''; + + classes.forEach(playerClass => { + const classOption = document.createElement('div'); + classOption.classList.add('class-option'); + if (playerClass._id === selectedClassId) { + classOption.classList.add('selected'); + } + classOption.innerHTML = `${playerClass.name}`; + + if (!playerClass.unlocked_by_default) { + classOption.classList.add('locked'); + const lockOverlay = document.createElement('div'); + lockOverlay.classList.add('lock-overlay'); + lockOverlay.innerHTML = ''; + classOption.appendChild(lockOverlay); + } + + classOption.addEventListener('click', () => selectClass(playerClass)); + classOption.addEventListener('mouseover', () => displayClassInfo(playerClass)); + classSelector.appendChild(classOption); + }); + + // Display info for the first unlocked class by default + const firstUnlockedClass = classes.find(c => c.unlocked_by_default); + if (firstUnlockedClass) { + displayClassInfo(firstUnlockedClass); + } else if (classes.length > 0) { + displayClassInfo(classes[0]); + } + }) + .catch(error => console.error('Error loading classes:', error)); +} + + +function displayClassInfo(playerClass) { + const classImage = document.getElementById('classImage'); + const className = document.getElementById('className'); + const classStats = document.getElementById('classStats'); + + classImage.innerHTML = `${playerClass.name}`; + className.textContent = playerClass.name; + classStats.innerHTML = ` +

Health: ${playerClass.base_attributes.health}

+

Speed: ${playerClass.base_attributes.speed}

+

Damage: ${playerClass.base_attributes.damage_multiplier}

+ `; + + // Add a visual indicator for locked classes in the info box + if (!playerClass.unlocked_by_default) { + className.innerHTML += ' (Locked)'; + } +} + + + + +function handleLevelUp() { + const upgradeOptions = getRandomUpgradeOptions(3); + showLevelUpPopup(upgradeOptions); +} + +function getRandomUpgradeOptions(count) { + const allUpgrades = [ + { type: 'bullet_speed', name: 'Bullet Speed', value: 1 }, + { type: 'bullet_size', name: 'Bullet Size', value: 1 }, + { type: 'damage', name: 'Damage', value: 2 }, + { type: 'fire_rate', name: 'Fire Rate', value: 0.1 } + ]; + return shuffleArray(allUpgrades).slice(0, count); +} + + +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +function pausePlayer() { + if (!isPlayerPaused) { + isPlayerPaused = true; + player.isPaused = true; + socket.emit('player_paused', { isPaused: true }); + } +} + +function unpausePlayer() { + if (isPlayerPaused && !areInterfacesOpen()) { + isPlayerPaused = false; + player.isPaused = false; + socket.emit('player_paused', { isPaused: false }); + } +} + + +function showLevelUpPopup(options) { + openInterfaces.levelUp = true; + pausePlayer(); + const popup = document.createElement('div'); + popup.id = 'levelUpPopup'; + popup.innerHTML = ` +

Level Up!

+

Choose an upgrade:

+
+ `; + document.body.appendChild(popup); + + const upgradeOptions = document.getElementById('upgradeOptions'); + options.forEach((option, index) => { + const button = document.createElement('button'); + button.classList.add('upgrade-button'); + button.innerHTML = ` + ${option.name} + +${option.value} + `; + button.onclick = () => selectUpgrade(option, index); + upgradeOptions.appendChild(button); + }); + + // Add slot machine effect + slotMachineEffect(options, upgradeOptions); +} + +function slotMachineEffect(options, container) { + const duration = 2000; // 2 seconds + const interval = 100; // 0.1 second per change + let elapsed = 0; + + const intervalId = setInterval(() => { + elapsed += interval; + if (elapsed < duration) { + container.innerHTML = ''; + getRandomUpgradeOptions(3).forEach(option => { + const div = document.createElement('div'); + div.classList.add('upgrade-option'); + div.innerHTML = ` + ${option.name} + +${option.value} + `; + container.appendChild(div); + }); + } else { + clearInterval(intervalId); + container.innerHTML = ''; + options.forEach((option, index) => { + const button = document.createElement('button'); + button.classList.add('upgrade-button'); + button.innerHTML = ` + ${option.name} + +${option.value} + `; + button.onclick = () => selectUpgrade(option, index); + container.appendChild(button); + }); + } + }, interval); +} + + +function selectUpgrade(upgrade, index) { + const popup = document.getElementById('levelUpPopup'); + popup.classList.add('fade-out'); + setTimeout(() => { + popup.remove(); + openInterfaces.levelUp = false; + unpausePlayer(); + }, 500); + // Send both the upgrade details and the index + socket.emit('upgrade_selected', { + upgradeIndex: index, + upgradeType: upgrade.type, + upgradeValue: upgrade.value + }); +} + + + +function areInterfacesOpen() { + return openInterfaces.menu || openInterfaces.levelUp || isShopVisible; + // Add any other interface checks as needed +} + + +function drawExperienceBar() { + if (!gameStarted) return; + + const expBar = document.getElementById('experienceBar'); + if (!expBar) { + const newExpBar = document.createElement('canvas'); + newExpBar.id = 'experienceBar'; + document.body.appendChild(newExpBar); + } + + const expBarCanvas = document.getElementById('experienceBar'); + const expBarCtx = expBarCanvas.getContext('2d'); + + const mapButton = document.getElementById('toggle-minimap'); + const mapButtonRect = mapButton.getBoundingClientRect(); + const shopButton = document.getElementById('shopButton'); + const shopButtonRect = shopButton.getBoundingClientRect(); + + let barWidth = Math.min(window.innerWidth * 0.8, 400); + const barHeight = Math.max(10, Math.min(window.innerHeight * 0.02, 20)); + + // Adjust width and position if on mobile + if (window.innerWidth <= 600) { + const availableWidth = window.innerWidth - shopButtonRect.width - mapButtonRect.width - 40; // 40px for padding + barWidth = Math.min(availableWidth, barWidth); + expBarCanvas.style.position = 'fixed'; + expBarCanvas.style.left = `${shopButtonRect.right + 10}px`; // 10px padding from shop button + expBarCanvas.style.bottom = '10px'; + } else { + expBarCanvas.style.position = 'fixed'; + expBarCanvas.style.left = '50%'; + expBarCanvas.style.bottom = '10px'; + expBarCanvas.style.transform = 'translateX(-50%)'; + } + + expBarCanvas.width = barWidth; + expBarCanvas.height = barHeight; + + // Draw background + expBarCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + expBarCtx.beginPath(); + expBarCtx.roundRect(0, 0, barWidth, barHeight, barHeight / 2); + expBarCtx.fill(); + + // Draw experience + const fillWidth = (playerExperience / playerMaxExperience) * barWidth; + expBarCtx.fillStyle = '#00ffff'; + expBarCtx.beginPath(); + expBarCtx.roundRect(0, 0, fillWidth, barHeight, barHeight / 2); + expBarCtx.fill(); + + // Add glow effect + expBarCtx.shadowColor = '#00ffff'; + expBarCtx.shadowBlur = 10; + expBarCtx.beginPath(); + expBarCtx.roundRect(0, 0, fillWidth, barHeight, barHeight / 2); + expBarCtx.stroke(); + expBarCtx.shadowBlur = 0; + + // Draw text + expBarCtx.fillStyle = 'white'; + expBarCtx.font = `${Math.max(10, Math.min(barHeight * 0.8, 16))}px Orbitron`; + expBarCtx.textAlign = 'center'; + expBarCtx.textBaseline = 'middle'; + + // Adjust text based on available space + const fullText = `Level ${playerLevel} - ${playerExperience}/${playerMaxExperience} XP`; + const shortText = `Lvl ${playerLevel} - ${playerExperience}/${playerMaxExperience}`; + const veryShortText = `Lvl ${playerLevel}`; + + let textToShow = fullText; + if (expBarCtx.measureText(fullText).width > barWidth) { + textToShow = shortText; + if (expBarCtx.measureText(shortText).width > barWidth) { + textToShow = veryShortText; + } + } + + expBarCtx.fillText(textToShow, barWidth / 2, barHeight / 2); +} + + + + +function checkSession() { + const sessionToken = localStorage.getItem('sessionToken'); + if (sessionToken) { + // Verify session with server + fetch('/verify-session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ sessionToken }), + }) + .then(response => response.json()) + .then(data => { + if (data.valid) { + currentUser = data.username; + updateAuthUI(true); + } else { + localStorage.removeItem('sessionToken'); + updateAuthUI(false); + } + }) + .catch(error => { + console.error('Error:', error); + updateAuthUI(false); + }); + } else { + updateAuthUI(false); + } +} + +function updateAuthUI(loggedIn) { + const authButtons = document.getElementById('authButtons'); + const logoutButton = document.getElementById('logoutButton'); + const playButton = document.getElementById('playButton'); + const welcomeMessage = document.getElementById('welcomeMessage'); + const signupForm = document.getElementById('signupForm'); + const loginForm = document.getElementById('loginForm'); + + if (loggedIn) { + authButtons.style.display = 'none'; + logoutButton.style.display = 'block'; // Changed from 'flex' to 'block' + playButton.textContent = 'Join Game'; + welcomeMessage.textContent = `Welcome to Resonance Rumble, ${currentUser}!`; + signupForm.style.display = 'none'; + loginForm.style.display = 'none'; + } else { + authButtons.style.display = 'flex'; + logoutButton.style.display = 'none'; + playButton.textContent = 'Join Game as Guest'; + welcomeMessage.textContent = 'Welcome to Resonance Rumble'; + // Optionally reset forms + signupForm.style.display = 'none'; + loginForm.style.display = 'none'; + } +} + + +function showSignupForm() { + document.getElementById('signupForm').style.display = 'flex'; + document.getElementById('loginForm').style.display = 'none'; +} + +function showLoginForm() { + document.getElementById('loginForm').style.display = 'flex'; + document.getElementById('signupForm').style.display = 'none'; +} + +function validateSignupInput(event) { + const input = event.target; + const value = input.value; + + if (input.id === 'signupUsername') { + // Username validation (e.g., at least 3 characters) + input.classList.toggle('valid', value.length >= 3); + input.classList.toggle('invalid', value.length < 3); + } else if (input.id === 'signupEmail') { + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + input.classList.toggle('valid', emailRegex.test(value)); + input.classList.toggle('invalid', !emailRegex.test(value)); + } else if (input.id === 'signupPassword') { + // Password validation (e.g., at least 8 characters) + input.classList.toggle('valid', value.length >= 8); + input.classList.toggle('invalid', value.length < 8); + } +} + +function signup() { + const username = document.getElementById('signupUsername').value; + const email = document.getElementById('signupEmail').value; + const password = document.getElementById('signupPassword').value; + + fetch('/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, email, password }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + notificationSystem.showNotification('Signup successful! Please log in.'); + showLoginForm(); + } else { + notificationSystem.showNotification(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + notificationSystem.showNotification('An error occurred during signup. Please try again.'); + }); +} + +function login() { + const username = document.getElementById('loginUsername').value; + const password = document.getElementById('loginPassword').value; + + fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + localStorage.setItem('sessionToken', data.sessionToken); + currentUser = username; + updateAuthUI(true); + } else { + notificationSystem.showNotification(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + notificationSystem.showNotification('An error occurred during login. Please try again.'); + }); +} + +function logout() { + localStorage.removeItem('sessionToken'); + currentUser = null; + updateAuthUI(false); +} + +function fetchUnlockedSkins() { + if (!currentUser) { + debugLog('No user logged in'); + return; + } + debugLog('Fetching unlocked skins for user:', currentUser); + fetch('/get-unlocked-skins', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: currentUser }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(unlockedSkins => { + debugLog('Fetched unlocked skins:', unlockedSkins); + displayUnlockedSkins(unlockedSkins); + }) + .catch(error => { + console.error('Error fetching unlocked skins:', error); + }); +} + +function displayUnlockedSkins(unlockedSkins) { + debugLog('Displaying unlocked skins:', unlockedSkins); + const colorOptions = document.getElementById('colorOptions'); + const hatOptions = document.getElementById('hatOptions'); + + if (!colorOptions || !hatOptions) { + console.error('Skin customization elements not found in the DOM'); + return; + } + + debugLog('Color options element:', colorOptions); + debugLog('Hat options element:', hatOptions); + + colorOptions.innerHTML = ''; + hatOptions.innerHTML = ''; + + unlockedSkins.forEach(skin => { + debugLog('Processing skin:', skin); + const optionElement = document.createElement('div'); + optionElement.classList.add('skin-option'); + optionElement.setAttribute('data-skin-id', skin.id); + optionElement.setAttribute('title', `${skin.name} (${skin.rarity})`); + + switch (skin.type) { + case 'color': + debugLog('Creating color option for:', skin.name); + optionElement.style.backgroundColor = skin.value; + if (skin.effect === 'glow') { + optionElement.classList.add('glow-effect'); + } + colorOptions.appendChild(optionElement); + break; + case 'hat': + debugLog('Creating hat option for:', skin.name); + optionElement.style.backgroundImage = `url(${skin.value})`; + hatOptions.appendChild(optionElement); + break; + } + + optionElement.addEventListener('click', () => selectSkin(skin.type, skin.id, optionElement)); + }); + + debugLog('Color options after processing:', colorOptions.innerHTML); + debugLog('Hat options after processing:', hatOptions.innerHTML); + + // Show a message if no skins are unlocked for a category + if (colorOptions.children.length === 0) { + colorOptions.innerHTML = '

No colors unlocked yet

'; + } + if (hatOptions.children.length === 0) { + hatOptions.innerHTML = '

No hats unlocked yet

'; + } +} + +function toggleInfoWidget() { + if (isInfoWidgetVisible) { + hideInfoWidget(); + } else { + showInfoWidget(); + } +} + +function showInfoWidget() { + infoWidget.classList.add('visible'); + isInfoWidgetVisible = true; + showInfoTab('socials'); // Open the Socials tab by default +} + +function hideInfoWidget() { + infoWidget.classList.remove('visible'); + isInfoWidgetVisible = false; +} + +function showInfoTab(tabName) { + const tabContents = infoWidget.querySelectorAll('.tab-content'); + tabContents.forEach(content => content.style.display = 'none'); + + const tabButtons = infoWidget.querySelectorAll('.tab-button'); + tabButtons.forEach(button => button.classList.remove('active')); + + const selectedTab = document.getElementById(tabName); + if (selectedTab) { + selectedTab.style.display = 'block'; + } + + const selectedButton = infoWidget.querySelector(`.tab-button[data-tab="${tabName}"]`); + if (selectedButton) { + selectedButton.classList.add('active'); + } +} + + +function toggleWidget(event) { + if (event) { + event.stopPropagation(); + } + if (uiWidget.classList.contains('visible')) { + hideWidget(); + } else { + showWidget(); + } +} + + +// Add event listeners for close buttons +document.querySelectorAll('.close-button').forEach(button => { + button.addEventListener('click', () => { + hideWidget(); + }); +}); + +// Modify the showWidget function +function showWidget() { + uiWidget.classList.add('visible'); + openInterfaces.menu = true; + if (gameStarted && player) { + pausePlayer(); + showTab('settings'); // Open the Settings tab by default when in-game + } else { + showTab('customization'); // Open the Customization tab by default in the main menu + } + updateAccountTab(); +} + +function updateAccountTab() { + const accountContent = document.getElementById('accountContent'); + if (currentUser) { + fetch('/get-discord-info', { + headers: { + 'Authorization': getSessionToken() + } + }) + .then(response => response.json()) + .then(data => { + if (data.linked) { + accountContent.innerHTML = ` +

Welcome, ${currentUser}!

+

Your Discord account is linked:

+
+ Discord Avatar + ${data.username} + +
+ `; + document.getElementById('unlinkDiscordButton').addEventListener('click', unlinkDiscord); + } else { + accountContent.innerHTML = ` +

Welcome, ${currentUser}!

+

Link your Discord account:

+ + `; + document.getElementById('linkDiscordButton').addEventListener('click', linkDiscord); + } + }) + .catch(error => { + console.error('Error fetching Discord info:', error); + accountContent.innerHTML = ` +

Welcome, ${currentUser}!

+

Error loading Discord information. Please try again later.

+ `; + }); + } else { + accountContent.innerHTML = ` + + `; + } +} + + +function getSessionToken() { + return localStorage.getItem('sessionToken'); +} + + +function linkDiscord() { + fetch('/initiate-discord-link', { + method: 'POST', + headers: { + 'Authorization': getSessionToken(), + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + const width = 600; + const height = 800; + const left = (screen.width / 2) - (width / 2); + const top = (screen.height / 2) - (height / 2); + + const popup = window.open(data.auth_url, 'discord-oauth', + `width=${width},height=${height},left=${left},top=${top}`); + + window.addEventListener('message', function(event) { + if (event.data === 'discord-linked') { + popup.close(); + updateAccountTab(); + notificationSystem.showNotification('Discord account linked successfully!'); + } + }, false); + }) + .catch(error => { + console.error('Error initiating Discord link:', error); + notificationSystem.showNotification('An error occurred while linking Discord account'); + }); +} + + +function unlinkDiscord() { + fetch('/unlink-discord', { + method: 'POST', + headers: { + 'Authorization': getSessionToken() + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + notificationSystem.showNotification('Discord account unlinked successfully'); + updateAccountTab(); + } else { + notificationSystem.showNotification('Failed to unlink Discord account'); + } + }) + .catch(error => { + console.error('Error unlinking Discord:', error); + notificationSystem.showNotification('An error occurred while unlinking Discord account'); + }); +} + + + +function onCustomizationTabOpen() { + const skinCustomization = document.getElementById('skinCustomization'); + if (!skinCustomization) { + console.error('skinCustomization element not found'); + return; + } + + // Show loading indicator immediately + skinCustomization.innerHTML = '
Loading skins...
'; + + // Load skin options asynchronously + setTimeout(() => { + loadSkinOptions(); + }, 0); +} + + + + +function hideWidget() { + uiWidget.classList.remove('visible'); + openInterfaces.menu = false; + if (gameStarted && player) { + unpausePlayer(); + } +} + +// Focus +// function handleOutsideClick(event) { +// const uiWidget = document.getElementById('uiWidget'); +// const classWidget = document.getElementById('classWidget'); +// const infoWidget = document.getElementById('infoWidget'); +// const menuButton = document.getElementById('menuButton'); +// const classSelectorButton = document.getElementById('classSelectorButton'); +// const infoButton = document.getElementById('infoButton'); + +// // Check and close UI Widget +// if (openInterfaces.menu && !uiWidget.contains(event.target) && event.target !== menuButton) { +// hideWidget(); +// } + +// // Check and close Class Widget +// if (classWidget.classList.contains('visible') && !classWidget.contains(event.target) && event.target !== classSelectorButton) { +// hideClassSelector(); +// } + +// // Check and close Info Widget +// if (isInfoWidgetVisible && !infoWidget.contains(event.target) && event.target !== infoButton) { +// hideInfoWidget(); +// } +// } + + +function showTab(tabName) { + debugLog('Showing tab:', tabName); + + // Hide all tab contents + const tabContents = document.querySelectorAll('.tab-content'); + tabContents.forEach(content => content.style.display = 'none'); + + // Deactivate all tab buttons + const tabButtons = document.querySelectorAll('.tab-button'); + tabButtons.forEach(button => button.classList.remove('active')); + + // Show the selected tab content + const selectedTab = document.getElementById(tabName); + if (selectedTab) { + selectedTab.style.display = 'block'; + } else { + console.error('No tab found with name:', tabName); + // If no valid tab is selected, default to the first tab + const firstTab = tabContents[0]; + if (firstTab) { + firstTab.style.display = 'block'; + tabName = firstTab.id; + } + } + + // Activate the selected tab button + const selectedButton = document.querySelector(`.tab-button[data-tab="${tabName}"]`); + if (selectedButton) { + selectedButton.classList.add('active'); + } + + if (tabName === 'customization') { + onCustomizationTabOpen(); + } else if (tabName === 'account') { + updateAccountTab(); // Update account tab content when selected + } +} + +function initAudio() { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; +} + + +function startGame() { + socket = io({ + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + reconnectionAttempts: 5 + }); + preloadEnemyImages(); + const playerName = currentUser || `Guest-${Math.floor(Math.random() * 10000)}`; + + socket.on('connect', () => { + debugLog('Connected to server'); + // Send join request with or without class_id + socket.emit('join', { + room: 'main_game_room', + name: playerName, + class_id: selectedClassId // This can be undefined if no class was selected + }); + }); + + + + socket.on('reconnect', (attemptNumber) => { + debugLog(`Reconnected to server after ${attemptNumber} attempts`); + // Re-join the game room + socket.emit('join', { + room: 'main_game_room', + name: playerName, + class_id: selectedClassId // This can be undefined if no class was selected + }); + }); + + + socket.on('disconnect', () => { + debugLog('Disconnected from server'); + // You might want to show a message to the user here + hideShopButton(); + }); + + socket.on('join_error', (data) => { + notificationSystem.showNotification(data.message); + // Disconnect the socket to prevent any further communication + socket.disconnect(); + // Reset game state + gameStarted = false; + document.getElementById('gameControls').style.display = 'flex'; + document.getElementById('ui').style.display = 'none'; + }); + + socket.on('reconnect_failed', () => { + debugLog('Failed to reconnect to server'); + // You might want to show a message to the user and/or reload the page + }); + + socket.on('damage_dealt', (data) => { + const damageText = Math.round(data.damage).toString(); + showFloatingText(damageText, data.enemyX, data.enemyY, '#ff0000', false); // Red color for damage, not moving up + }); + + socket.on('music_sync', (data) => { + if (gameStarted) { + const serverClientTimeDiff = Date.now() / 1000 - data.serverTime; + currentSongDuration = data.songDuration || 180; // Use the provided duration or default to 3 minutes + const songPosition = (Date.now() / 1000 - data.startTime - serverClientTimeDiff) % currentSongDuration; + setupAudio(data.song, songPosition); + } + }); + + socket.on('music_stop', () => { + if (audioElement) { + audioElement.pause(); + } + }); + + socket.on('game_alert', (data) => { + if (gameStarted) { + alertSystem.addnotificationSystem.showNotification(data.message); + } + }); + + socket.on('game_state', (data) => { + debugLog('Received game state:', data); + player = new Player( + data.players[socket.id].x, + data.players[socket.id].y, + playerName, + data.players[socket.id].skin, + data.players[socket.id].weapons, + data.players[socket.id].player_class, + ctx + ); + player.level = data.players[socket.id].level || 1; + player.experience = data.players[socket.id].experience || 0; + player.maxExperience = data.players[socket.id].max_experience || 100; + player.isPaused = data.players[socket.id].is_paused || false; + player.synthCoins = data.players[socket.id].synth_coins || 0; + player.currentHealth = data.players[socket.id].current_health; + player.maxHealth = data.players[socket.id].max_health; + updateSynthCoinsDisplay(); + debugLog(`Initial player stats: Level ${player.level}, Exp ${player.experience}/${player.maxExperience}, Health ${player.currentHealth}/${player.maxHealth}`); + + player.weapons.forEach(weapon => { + if (weapon && weapon.bullet_image_source) { + preloadBulletImage(weapon.bullet_image_source); + } else { + console.error('Invalid weapon structure:', weapon); + } + }); + + + loadPlayerSkin(playerName); + + otherPlayers = Object.entries(data.players).reduce((acc, [id, playerData]) => { + if (id !== socket.id) { + acc[id] = new Player( + playerData.x, + playerData.y, + playerData.name, + playerData.skin, + playerData.weapon, + playerData.player_class, + ctx // Pass the canvas context here + ); + acc[id].level = playerData.level || 1; + acc[id].isPaused = playerData.is_paused || false; + loadPlayerSkin(playerData.name); + } + return acc; + }, {}); + + // Preload bullet images for all weapons + Object.values(data.players).forEach(playerData => { + if (playerData.weapon && playerData.weapon.bullet_image_source) { + preloadBulletImage(playerData.weapon.bullet_image_source); + } + }); + + serverEnemies = data.enemies.map(enemyData => new Enemy(enemyData)); + serverBullets = data.bullets; + + gameStarted = true; + document.getElementById('gameControls').style.display = 'none'; + document.getElementById('ui').style.display = 'flex'; + document.getElementById('minimap-container').style.display = 'block'; + infoButton.style.display = 'none'; + hideInfoWidget(); + + showShopButton(); // Show the shop button when the game starts + + socket.emit('request_music_sync'); + }); + + socket.on('synth_coins_update', (data) => { + synthCoins = data.coins.map(coin => new SynthCoin(coin.x, coin.y)); + }); + + socket.on('new_synth_coin', (data) => { + synthCoins.push(new SynthCoin(data.x, data.y)); + }); + + socket.on('coin_collected', (data) => { + debugLog('Coin collected event received:', data); + updateCoinCollection(data); + }); + + socket.on('collect_synth_coin', (data) => { + debugLog('Collect synth coin event received:', data); + updateCoinCollection(data); + }); + + socket.on('player_update', (data) => { + if (data.id !== socket.id) { + if (!otherPlayers[data.id]) { + otherPlayers[data.id] = new Player( + data.player.x, + data.player.y, + data.player.name, + data.player.skin, + data.player.weapons, + data.player.player_class, + ctx + ); + } else { + const updatedPlayer = otherPlayers[data.id]; + Object.assign(updatedPlayer, data.player); + updatedPlayer.setSkin(data.player.skin); + updatedPlayer.weapons = data.player.weapons; + updatedPlayer.loadWeaponImages(); + updatedPlayer.weaponAngles = data.player.weapon_angles || updatedPlayer.weapons.map(() => 0); + updatedPlayer.currentHealth = data.player.current_health; + updatedPlayer.maxHealth = data.player.max_health; + updatedPlayer.isPaused = data.player.is_paused; + updatedPlayer.autoFireEnabled = data.player.auto_fire_enabled; + } + } else { + // Update the current player's weapons if necessary + player.weapons = data.player.weapons; + player.loadWeaponImages(); + player.weaponAngles = data.player.weapon_angles || player.weapons.map(() => 0); + player.currentHealth = data.player.current_health; + player.maxHealth = data.player.max_health; + player.isPaused = data.player.is_paused; + player.autoFireEnabled = data.player.auto_fire_enabled; + } + }); + + + socket.on('upgrade_applied', (data) => { + debugLog('Upgrade applied:', data); + if (player) { + player.upgradeWeapon(data.attribute, data.value); + } else { + console.error('Player object not found'); + } + }); + + + socket.on('new_player', (data) => { + debugLog('New player joined:', data); + if (data.id !== socket.id) { + otherPlayers[data.id] = new Player( + data.player.x, + data.player.y, + data.player.name, + data.player.skin, + data.player.weapon, + data.player.player_class, + ctx + ); + loadPlayerSkin(data.player.name); + } + }); + + + socket.on('player_shoot', (data) => { + if (data.playerId in otherPlayers) { + const player = otherPlayers[data.playerId]; + for (let i = 0; i < player.weaponAttributes.count; i++) { + const angle = data.angle + (Math.random() - 0.5) * 0.2; // Add some spread + const bullet = { + x: player.x, + y: player.y, + angle: angle, + speed: player.weaponAttributes.speed, + size: player.weaponAttributes.size, + damage: player.weaponAttributes.damage, + playerId: data.playerId + }; + serverBullets.push(bullet); + } + } + }); + + + socket.on('experience_update', (data) => { + if (data.player_id === socket.id) { + playerExperience = data.experience; + playerMaxExperience = data.max_experience; + playerLevel = data.level; + player.level = data.level; + + debugLog(`Experience updated: ${playerExperience}/${playerMaxExperience}, Level: ${playerLevel}`); + + if (data.leveled_up) { + debugLog('Level up!'); + handleLevelUp(); + } + + if (data.exp_gained) { + showFloatingText(`+${data.exp_gained} XP`, player.x, player.y - 30, '#FFFFFF', true); // Gold color for XP, moving up + } + } else if (otherPlayers[data.player_id]) { + otherPlayers[data.player_id].level = data.level; + } + }); + + socket.on('player_disconnected', (playerId) => { + delete otherPlayers[playerId]; + }); + + socket.on('game_update', (data) => { + serverEnemies = data.enemies.map(enemyData => { + const enemy = new Enemy(enemyData); + enemy.update(); // Ensure the enemy's trail is initialized + return enemy; + }); + serverBullets = data.bullets; + + // Update synth coins + synthCoins = data.synth_coins.map(coinData => { + const existingCoin = synthCoins.find(c => Math.hypot(c.x - coinData.x, c.y - coinData.y) < 0.1); + if (existingCoin) { + existingCoin.x = coinData.x; + existingCoin.y = coinData.y; + return existingCoin; + } else { + return new SynthCoin(coinData.x, coinData.y); + } + }); + // Update player health + if (data.players[socket.id]) { + player.currentHealth = data.players[socket.id].current_health; + player.maxHealth = data.players[socket.id].max_health; + } + + // Update other players' health + Object.entries(data.players).forEach(([id, playerData]) => { + if (id !== socket.id && otherPlayers[id]) { + otherPlayers[id].currentHealth = playerData.current_health; + otherPlayers[id].maxHealth = playerData.max_health; + } + }); +}); + +socket.on('bullet_impact', (data) => { + impactEffects.push(new ImpactEffect(data.x, data.y, data.color, data.size, data.angle)); +}); + + + socket.on('new_bullet', (bulletData) => { + serverBullets.push(bulletData); + if (bulletData.bullet_image_source) { + preloadBulletImage(bulletData.bullet_image_source); + } + }); + + socket.on('enemy_destroyed', (data) => { + data.particles.forEach(particleData => { + particles.push(new Particle( + particleData.x, + particleData.y, + particleData.color, + particleData.radius, + particleData.velocityX, + particleData.velocityY, + particleData.ttl + )); + }); + }); + + socket.on('player_died', (data) => { + if (data.player_id === socket.id) { + // This is the current player + console.log('You died!'); + // Implement any client-side death logic here (e.g., death animation, respawn timer) + } else { + // Another player died + console.log(`Player ${data.player_id} died`); + // Optionally implement logic for when other players die + } + }); + + + socket.on('music_control', (data) => { + debugLog('Received music control:', data); + switch(data.action) { + case 'start': + case 'change': + const currentTime = Date.now() / 1000; + const elapsedTime = currentTime - data.startTime; + currentSongDuration = data.songDuration || 180; // Use the provided duration or default to 3 minutes + setupAudio(data.song, elapsedTime % currentSongDuration); + break; + case 'stop': + if (audioElement) { + audioElement.pause(); + } + break; + } + }); + + socket.on('skin_update', (data) => { + const { player_id, skin_data } = data; + if (player_id === socket.id) { + // Update the current player's skin + player.setSkin(skin_data); + } else if (otherPlayers[player_id]) { + // Update other player's skin + otherPlayers[player_id].setSkin(skin_data); + } + }); + +} + +function hideShopButton() { + const shopButton = document.getElementById('shopButton'); + if (shopButton) { + shopButton.style.display = 'none'; + } +} + + +function updateCoinCollection(data) { + if (data.player_id === socket.id) { + const coinsCollected = data.coin_count - player.synthCoins; + player.synthCoins = data.coin_count; + updateSynthCoinsDisplay(); + debugLog('Updated player synth coins:', player.synthCoins); + + // Create or update floating text for coin collection + if (currentCoinFloatingText) { + currentCoinFloatingText.amount += coinsCollected; + currentCoinFloatingText.text = `+$${currentCoinFloatingText.amount}`; + currentCoinFloatingText.lifetime = 60; // Reset lifetime + currentCoinFloatingText.y = player.y - 50; // Update position + } else { + currentCoinFloatingText = { + text: `+$${coinsCollected}`, + x: player.x, + y: player.y - 50, + color: '#FFD700', // Gold color + lifetime: 60, + velocity: -0.5, + amount: coinsCollected + }; + floatingTexts.push(currentCoinFloatingText); + } + debugLog('Floating text created:', currentCoinFloatingText); + } else if (otherPlayers[data.player_id]) { + otherPlayers[data.player_id].synthCoins = data.coin_count; + } + // Remove the collected coin from the synthCoins array + synthCoins = synthCoins.filter(coin => coin.x !== data.coin_x || coin.y !== data.coin_y); +} + + +function showFloatingText(text, x, y, color, moveUp = true) { + const floatingText = { + text: text, + x: x, + y: y, + color: color, + lifetime: 60, // Number of frames the text will be visible + velocity: moveUp ? -1 : 0 // Upward velocity if moveUp is true, otherwise 0 + }; + floatingTexts.push(floatingText); +} + +// Add an array to store floating texts + + +// Add a function to load player skins +function loadPlayerSkin(username) { + fetch('/get-player-skin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(skinData => { + debugLog('Received skin data:', skinData); // Add this line for debugging + if (player && player.name === username) { + player.setSkin(skinData); + } + // Update other players' skins if necessary + Object.values(otherPlayers).forEach(otherPlayer => { + if (otherPlayer.name === username) { + otherPlayer.setSkin(skinData); + } + }); + }) + .catch(error => { + console.error('Error loading player skin:', error); + // Set a default skin if there's an error + const defaultSkin = { + color: { name: 'Default Blue', value: '#00ffff', effect: null } + }; + if (player && player.name === username) { + player.setSkin(defaultSkin); + } + Object.values(otherPlayers).forEach(otherPlayer => { + if (otherPlayer.name === username) { + otherPlayer.setSkin(defaultSkin); + } + }); + }); +} + +// Update the setupAudio function +function setupAudio(audioSource, startTime = 0) { + if (audioElement) { + audioElement.pause(); + } + audioElement = new Audio(audioSource); + audioElement.currentTime = startTime; + audioElement.play().catch(e => console.error("Error playing audio:", e)); + + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + + if (!analyser) { + analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + audioData = new Uint8Array(analyser.frequencyBinCount); + } + + if (!source) { + source = audioContext.createMediaElementSource(audioElement); + source.connect(analyser); + analyser.connect(audioContext.destination); + } +} + +// Add this new function +function toggleMinimap() { + minimapState = (minimapState + 1) % 3; + const minimap = document.getElementById('minimap'); + + switch (minimapState) { + case 0: + minimap.classList.remove('small', 'large'); + break; + case 1: + minimap.classList.add('small'); + minimap.classList.remove('large'); + break; + case 2: + minimap.classList.remove('small'); + minimap.classList.add('large'); + break; + } + } + +// Add this new function +function drawMinimap() { + if (minimapState === 0) return; + + const minimap = document.getElementById('minimap'); + const scale = minimapState === 1 ? 0.05 : 0.2; + + minimapCanvas.width = minimap.offsetWidth; + minimapCanvas.height = minimap.offsetHeight; + + minimapCtx.fillStyle = 'rgba(18, 4, 88, 0.7)'; + minimapCtx.fillRect(0, 0, minimapCanvas.width, minimapCanvas.height); + + // Draw player + const playerX = (player.x / MAP_WIDTH) * minimapCanvas.width; + const playerY = (player.y / MAP_HEIGHT) * minimapCanvas.height; + minimapCtx.fillStyle = '#00ffff'; + minimapCtx.beginPath(); + minimapCtx.arc(playerX, playerY, 3, 0, Math.PI * 2); + minimapCtx.fill(); + + // Draw player name on large minimap + if (minimapState === 2) { + drawPlayerName(minimapCtx, player.name, playerX, playerY, '#00ffff'); + } + + // Draw other players + minimapCtx.fillStyle = '#ff00ff'; + Object.values(otherPlayers).forEach(otherPlayer => { + const x = (otherPlayer.x / MAP_WIDTH) * minimapCanvas.width; + const y = (otherPlayer.y / MAP_HEIGHT) * minimapCanvas.height; + minimapCtx.beginPath(); + minimapCtx.arc(x, y, 2, 0, Math.PI * 2); + minimapCtx.fill(); + + // Draw other player names on large minimap + if (minimapState === 2) { + drawPlayerName(minimapCtx, otherPlayer.name, x, y, '#ff00ff'); + } + }); + + // Draw enemies + minimapCtx.fillStyle = '#ff0000'; + serverEnemies.forEach(enemy => { + const x = (enemy.x / MAP_WIDTH) * minimapCanvas.width; + const y = (enemy.y / MAP_HEIGHT) * minimapCanvas.height; + minimapCtx.beginPath(); + minimapCtx.arc(x, y, 2, 0, Math.PI * 2); + minimapCtx.fill(); + }); + } + + // New helper function to draw player names + function drawPlayerName(ctx, name, x, y, color) { + ctx.font = '12px Orbitron'; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; + ctx.fillText(name, x, y - 5); + } + + class SynthCoin { + constructor(x, y) { + this.x = x; + this.y = y; + this.radius = 10; + this.angle = Math.random() * Math.PI * 2; + this.glowAngle = 0; + } + + update() { + this.angle += 0.02; + this.glowAngle += 0.1; + } + + draw(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.angle); + + // Draw coin base + const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.radius); + gradient.addColorStop(0, '#FFD700'); // Center: Gold + gradient.addColorStop(0.8, '#DAA520'); // Edge: Goldenrod + gradient.addColorStop(1, '#B8860B'); // Rim: Dark Goldenrod + + ctx.beginPath(); + ctx.arc(0, 0, this.radius, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.fill(); + + // Draw coin edge + ctx.beginPath(); + ctx.arc(0, 0, this.radius, 0, Math.PI * 2); + ctx.strokeStyle = '#B8860B'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw "$" symbol + ctx.fillStyle = '#B8860B'; + ctx.font = `bold ${this.radius}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('$', 0, 0); + + // Draw glow effect + const glowSize = this.radius * 1.5; + const glowGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, glowSize); + glowGradient.addColorStop(0, 'rgba(255, 215, 0, 0.5)'); + glowGradient.addColorStop(1, 'rgba(255, 215, 0, 0)'); + + ctx.globalAlpha = 0.5 + Math.sin(this.glowAngle) * 0.2; // Pulsating glow + ctx.beginPath(); + ctx.arc(0, 0, glowSize, 0, Math.PI * 2); + ctx.fillStyle = glowGradient; + ctx.fill(); + + // Draw shine effect + ctx.globalAlpha = 0.7; + ctx.beginPath(); + ctx.ellipse(-this.radius * 0.3, -this.radius * 0.3, this.radius * 0.2, this.radius * 0.4, Math.PI / 4, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fill(); + + ctx.restore(); + } +} + +let synthCoins = []; + + +// Star class +class Star { + constructor() { + this.reset(); + } + + reset() { + this.x = Math.random() * MAP_WIDTH; + this.y = Math.random() * MAP_HEIGHT; + this.size = Math.random() * 3 + 1; + this.speed = Math.random() * 0.5 + 0.1; + } + + update(audioData) { + this.y += this.speed; + if (this.y > MAP_HEIGHT) { + this.reset(); + this.y = 0; // Start from the top again + } + + // Audio visualization effect + const index = Math.floor(this.x / MAP_WIDTH * audioData.length); + const audioIntensity = audioData[index] / 255; + this.size = (Math.random() * 3 + 1) * (1 + audioIntensity); + } + + draw(ctx) { + ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.5 + 0.5})`; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fill(); + } +} + +// Shooting Star class +class ShootingStar { + constructor() { + this.reset(); + } + + reset() { + this.x = Math.random() * MAP_WIDTH; + this.y = 0; + this.length = Math.random() * 80 + 20; + this.speed = Math.random() * 10 + 5; + this.angle = Math.PI / 4 + (Math.random() * Math.PI / 4); + this.active = true; + this.tailPoints = []; + } + + update(audioData) { + this.x += Math.cos(this.angle) * this.speed; + this.y += Math.sin(this.angle) * this.speed; + + // Audio visualization effect + const index = Math.floor(this.x / MAP_WIDTH * audioData.length); + const audioIntensity = audioData[index] / 255; + this.speed = (Math.random() * 10 + 5) * (1 + audioIntensity); + + this.tailPoints.unshift({x: this.x, y: this.y}); + if (this.tailPoints.length > 20) { + this.tailPoints.pop(); + } + + if (this.x > MAP_WIDTH || this.y > MAP_HEIGHT) { + this.reset(); + } + } + + draw(ctx) { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(this.x, this.y); + for (let i = 0; i < this.tailPoints.length; i++) { + const point = this.tailPoints[i]; + ctx.lineTo(point.x, point.y); + ctx.globalAlpha = 1 - (i / this.tailPoints.length); + } + ctx.stroke(); + ctx.globalAlpha = 1; + } +} + +class NeonCloud { + constructor(x, y, size) { + this.x = x; + this.y = y; + this.size = size; + this.baseColor = this.getRandomNeonColor(); + this.offset = Math.random() * Math.PI * 2; + } + + getRandomNeonColor() { + const neonColors = [ + '#ff00ff', // Magenta + '#00ffff', // Cyan + '#ff00a0', // Hot Pink + '#00ff7f', // Spring Green + '#ff6b6b', // Light Red + '#4deeea', // Bright Turquoise + ]; + return neonColors[Math.floor(Math.random() * neonColors.length)]; + } + + update(audioData) { + const index = Math.floor(this.x / MAP_WIDTH * audioData.length); + const audioIntensity = audioData[index] / 255; + this.size = this.size * (1 + audioIntensity * 0.2); + } + + draw(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size); + gradient.addColorStop(0, this.baseColor + '77'); // Semi-transparent + gradient.addColorStop(1, this.baseColor + '00'); // Fully transparent + + ctx.beginPath(); + ctx.arc(0, 0, this.size, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.globalAlpha = 0.7 + Math.sin(Date.now() / 1000 + this.offset) * 0.3; // Pulsating effect + ctx.fill(); + + ctx.restore(); + } +} + + +// Background effects manager +class BackgroundEffects { + constructor() { + this.stars = Array(200).fill().map(() => new Star()); + this.shootingStars = Array(5).fill().map(() => new ShootingStar()); + this.neonClouds = Array(10).fill().map(() => new NeonCloud( + Math.random() * MAP_WIDTH, + Math.random() * MAP_HEIGHT, + Math.random() * 100 + 50 + )); + } + + update(audioData) { + this.stars.forEach(star => star.update(audioData)); + this.shootingStars.forEach(star => { + if (star.active) { + star.update(audioData); + } else { + star.reset(); + } + }); + this.neonClouds.forEach(cloud => cloud.update(audioData)); + } + + draw(ctx) { + // Draw neon clouds first, so they appear behind other elements + this.neonClouds.forEach(cloud => cloud.draw(ctx)); + this.stars.forEach(star => star.draw(ctx)); + this.shootingStars.forEach(star => star.draw(ctx)); + } +} + +class AlertSystem { + constructor() { + this.alerts = []; + this.alertDuration = 3000; // Duration in milliseconds + } + + // addnotificationSystem.showNotification(message) { + // const alert = { + // message: message, + // createdAt: Date.now(), + // opacity: 1 + // }; + // this.alerts.push(alert); + // } + + update() { + const currentTime = Date.now(); + this.alerts = this.alerts.filter(alert => { + const elapsedTime = currentTime - alert.createdAt; + if (elapsedTime > this.alertDuration) { + return false; + } + if (elapsedTime > this.alertDuration / 2) { + alert.opacity = 1 - ((elapsedTime - this.alertDuration / 2) / (this.alertDuration / 2)); + } + return true; + }); + } + + draw(ctx) { + const alertHeight = 30; + const padding = 10; + let yOffset = padding; + + ctx.save(); + ctx.font = '16px Orbitron'; + ctx.textAlign = 'center'; + + this.alerts.forEach(alert => { + ctx.fillStyle = `rgba(255, 255, 255, ${alert.opacity})`; + ctx.fillText(alert.message, canvas.width / 2, yOffset + alertHeight / 2); + yOffset += alertHeight + padding; + }); + + ctx.restore(); + } +} + +// Create an instance of the AlertSystem +const alertSystem = new AlertSystem(); + +// Create an instance of BackgroundEffects +const backgroundEffects = new BackgroundEffects(); + + +// Modify the update function +function update() { + // Clear main canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (!gameStarted || !player) { + requestAnimationFrame(update); + return; + } + + const currentTime = Date.now(); + const deltaTime = (currentTime - lastUpdateTime) / 1000; // Convert to seconds + lastUpdateTime = currentTime; + + + if (gameStarted && player) { + if (!areInterfacesOpen()) { + if (isAutoFireEnabled) { + const nearestEnemy = findNearestEnemy(); + player.updateWeaponAngle(nearestEnemy); + player.shoot(currentTime, nearestEnemy); + } else { + // Handle manual aiming and shooting + player.updateWeaponAngleToMouse(); + if (player.isShooting) { + player.shoot(currentTime); + } + } + } + + + socket.emit('player_update', { + x: player.x, + y: player.y, + weapon_angles: player.weaponAngles, + auto_fire_enabled: isAutoFireEnabled + }); + + updateAndDrawShootingJoystick(deltaTime); + } + + if (gameStarted) { + alertSystem.update(); + } + + if (gameStarted && player && !player.isPaused) { + const allPlayers = { ...otherPlayers, [socket.id]: player }; + synthCoins.forEach(coin => { + coin.update(allPlayers); + }); + } + + impactEffects = impactEffects.filter(effect => { + effect.update(); + return effect.lifetime > 0; + }); + + + + // Update Synth Coins + synthCoins.forEach(coin => coin.update()); + + + // Check for coin collection only if the player is not paused + if (!player.isPaused) { + synthCoins = synthCoins.filter(coin => { + const distance = Math.hypot(player.x - coin.x, player.y - coin.y); + if (distance <= player.radius + coin.radius) { + // Coin collected + socket.emit('collect_synth_coin', { coin_x: coin.x, coin_y: coin.y }); + return false; + } + return true; + }); + } + + + player.update(); + camera.follow(player); + + serverEnemies.forEach(enemy => enemy.update()); + + ctx.save(); + ctx.translate(-camera.x, -camera.y); + + // Draw the map boundaries + ctx.strokeStyle = 'white'; + ctx.strokeRect(0, 0, MAP_WIDTH, MAP_HEIGHT); + + // Update audio data + if (analyser) { + analyser.getByteFrequencyData(audioData); + } + + // Update and draw background effects + backgroundEffects.update(audioData); + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + backgroundEffects.draw(ctx); + ctx.globalCompositeOperation = 'source-over'; + ctx.restore(); + + // Draw game objects + drawGameObjects(); + + updateSynthCoinsDisplay(); + + ctx.restore(); + + // Draw UI elements (like joystick) after restoring the context + if (joystick.active) { + drawJoystick(); + } + + drawMinimap(); + drawExperienceBar(); + + // If the widget is visible, draw a semi-transparent overlay + if (isWidgetVisible) { + ctx.fillStyle = 'rgba(18, 4, 88, 0.5)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + socket.emit('player_update', { + x: player.x, + y: player.y, + auto_fire_enabled: isAutoFireEnabled // Send auto-fire preference + }); + if (gameStarted) { + alertSystem.draw(ctx); + } + requestAnimationFrame(update); + debugLog('Player position:', player.x, player.y); + debugLog('Number of enemies:', serverEnemies.length); + +} + +function stopPropagationOnUIElements() { + const uiElements = document.querySelectorAll('#uiWidget, #classWidget, #infoWidget, #shopWidget, #mobileControls'); + uiElements.forEach(element => { + element.addEventListener('mousedown', (e) => e.stopPropagation()); + element.addEventListener('touchstart', (e) => e.stopPropagation()); + }); +} + + + +function lerpAngle(a, b, t) { + const diff = b - a; + const adjusted = ((diff + Math.PI) % (2 * Math.PI)) - Math.PI; + return a + adjusted * t; +} + +function findNearestEnemy() { + let nearestEnemy = null; + let nearestDistance = Infinity; + + serverEnemies.forEach(enemy => { + const distance = Math.hypot(enemy.x - player.x, enemy.y - player.y); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestEnemy = enemy; + } + }); + + return nearestEnemy; +} + + +// New function to draw game objects +function drawGameObjects() { + if (!ctx) { + console.error('Canvas context is undefined in drawGameObjects'); + return; + } + + // Draw server-managed enemies + serverEnemies.forEach(enemy => { + if (isVisible(enemy.x, enemy.y)) { + enemy.draw(ctx); + } + }); + + // Draw server-managed bullets + serverBullets.forEach(bullet => { + if (isVisible(bullet.x, bullet.y)) { + const bulletImage = bulletImageCache.get(bullet.bullet_image_source); + if (bulletImage && bulletImage.complete) { + ctx.save(); + ctx.translate(bullet.x, bullet.y); + ctx.rotate(bullet.angle); + ctx.drawImage(bulletImage, -bullet.size/2, -bullet.size/2, bullet.size, bullet.size); + ctx.restore(); + } else { + // Fallback to drawing a circle if the image is not loaded + ctx.beginPath(); + ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); + ctx.fillStyle = bullet.color || '#ffff00'; // Use the bullet's color or default to yellow + ctx.fill(); + ctx.closePath(); + } + } + }); + + impactEffects.forEach(effect => { + if (isVisible(effect.x, effect.y)) { + effect.draw(ctx); + } + }); + + + // Draw other players + Object.values(otherPlayers).forEach(otherPlayer => { + if (isVisible(otherPlayer.x, otherPlayer.y)) { + otherPlayer.drawWeapons(); + otherPlayer.draw(); + } + }); + + // Draw Synth Coins + synthCoins.forEach(coin => { + if (isVisible(coin.x, coin.y)) { + coin.draw(ctx); + } + }); + + // Draw the player + if (player) { + player.drawWeapons(); + player.draw(); + } + + + // Update and draw particles + particles.forEach((particle, index) => { + particle.update(); + if (isVisible(particle.x, particle.y)) { + particle.draw(); + } + if (particle.ttl <= 0) { + particles.splice(index, 1); + } + }); + + // Update and draw floating texts + floatingTexts = floatingTexts.filter((text, index) => { + text.y += text.velocity; + text.lifetime--; + + if (text.lifetime <= 0) { + if (text === currentCoinFloatingText) { + currentCoinFloatingText = null; + } + return false; + } + + if (isVisible(text.x, text.y)) { + ctx.font = '16px Orbitron'; + ctx.fillStyle = text.color; + ctx.textAlign = 'center'; + + let drawX = text.x; + let drawY = text.y; + + // For damage text (not moving up), add a slight random offset + if (text.velocity === 0) { + drawX += (Math.random() - 0.5) * 2; // Random X offset + drawY += (Math.random() - 0.5) * 2; // Random Y offset + } + + ctx.fillText(text.text, drawX, drawY); + } + + return true; + }); +} + + +function selectClass(playerClass) { + if (!playerClass.unlocked_by_default && currentUser === null) { + notificationSystem.showNotification('You need to be logged in to select this class.'); + return; + } + + if (!playerClass.unlocked_by_default) { + showUnlockRequirements(playerClass); + return; + } + + selectedClassId = playerClass._id; + const classSelector = document.getElementById('classSelector'); + const classOptions = classSelector.querySelectorAll('.class-option'); + classOptions.forEach(option => option.classList.remove('selected')); + const selectedOption = classSelector.querySelector(`.class-option:nth-child(${Array.from(classOptions).indexOf(event.currentTarget) + 1})`); + if (selectedOption) { + selectedOption.classList.add('selected'); + } + + if (currentUser) { + // Only send selection to server if user is logged in + fetch('/select-class', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: currentUser, class_id: selectedClassId }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + debugLog('Class selected successfully'); + } else { + console.error('Failed to select class:', data.error); + notificationSystem.showNotification('Failed to select class. Please try again.'); + } + }) + .catch(error => { + console.error('Error selecting class:', error); + notificationSystem.showNotification('An error occurred while selecting the class. Please try again.'); + }); + } +} + +function debugShopWidget() { + debugLog('Debugging Shop Widget:'); + const shopWidget = document.getElementById('shopWidget'); + debugLog('Shop Widget:', shopWidget); + debugLog('Shop Widget Classes:', shopWidget.classList); + + const weaponSelector = document.getElementById('weaponSelector'); + debugLog('Weapon Selector:', weaponSelector); + if (weaponSelector) { + debugLog('Weapon Selector Children:', weaponSelector.children); + debugLog('Weapon Selector Display:', getComputedStyle(weaponSelector).display); + } + + const shopTab = document.querySelector('#shopWidget .tab-button[data-tab="shop"]'); + debugLog('Shop Tab:', shopTab); + if (shopTab) { + debugLog('Shop Tab Classes:', shopTab.classList); + } + + const shopContent = document.querySelector('#shopWidget #shop'); + debugLog('Shop Content:', shopContent); + if (shopContent) { + debugLog('Shop Content Display:', getComputedStyle(shopContent).display); + } + + debugLog('Player:', player); + if (player && player.playerClass) { + debugLog('Player Class ID:', player.playerClass._id); + } +} + + +function showUnlockRequirements(playerClass) { + const requirementsPopup = document.createElement('div'); + requirementsPopup.classList.add('requirements-popup'); + requirementsPopup.innerHTML = ` +

${playerClass.name} Unlock Requirements

+
    + ${Object.entries(playerClass.unlock_requirements).map(([key, value]) => `
  • ${key}: ${value}
  • `).join('')} +
+ + `; + document.body.appendChild(requirementsPopup); + document.getElementById('closeRequirements').addEventListener('click', () => requirementsPopup.remove()); +} + + +function isVisible(x, y) { + return x >= camera.x - 50 && x <= camera.x + camera.width + 50 && + y >= camera.y - 50 && y <= camera.y + camera.height + 50; +} + +function drawJoystick() { + const joystickX = joystick.startX; + const joystickY = joystick.startY; + ctx.beginPath(); + ctx.arc(joystickX, joystickY, 40, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; + ctx.fill(); + ctx.closePath(); + + ctx.beginPath(); + ctx.arc(joystick.endX, joystick.endY, 20, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fill(); + ctx.closePath(); +} + +function handleKeyDown(e) { + if (!gameStarted) return; + switch(e.key) { + case 'ArrowUp': + case 'w': + case 'W': + player.keys.up = true; + break; + case 'ArrowDown': + case 's': + case 'S': + player.keys.down = true; + break; + case 'ArrowLeft': + case 'a': + case 'A': + player.keys.left = true; + break; + case 'ArrowRight': + case 'd': + case 'D': + player.keys.right = true; + break; + } +} + +function handleKeyUp(e) { + if (!gameStarted) return; + switch(e.key) { + case 'ArrowUp': + case 'w': + case 'W': + player.keys.up = false; + break; + case 'ArrowDown': + case 's': + case 'S': + player.keys.down = false; + break; + case 'ArrowLeft': + case 'a': + case 'A': + player.keys.left = false; + break; + case 'ArrowRight': + case 'd': + case 'D': + player.keys.right = false; + break; + } +} + +let moveTouch = null; +let aimTouch = null; + +function handleTouchStart(e) { + if (!gameStarted || !player) return; + + // Check if the touch is on a UI element + if (e.target.closest('#uiWidget, #classWidget, #infoWidget, #shopWidget, #mobileControls')) { + return; + } + + e.preventDefault(); + + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + if (touch.clientX < window.innerWidth / 2) { + // Left half of the screen - movement + moveTouch = touch; + joystick.active = true; + joystick.startX = joystick.endX = touch.clientX; + joystick.startY = joystick.endY = touch.clientY; + } else { + // Right half of the screen - aiming and shooting + aimTouch = touch; + shootingJoystick.active = true; + shootingJoystick.startX = shootingJoystick.currentX = touch.clientX; + shootingJoystick.startY = shootingJoystick.currentY = touch.clientY; + shootingJoystick.innerRadius = 0; + shootingJoystick.aimAngle = 0; + } + } +} + +function handleTouchMove(e) { + e.preventDefault(); + if (!gameStarted || !player) return; + + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + if (touch.identifier === moveTouch?.identifier) { + // Movement + joystick.endX = touch.clientX; + joystick.endY = touch.clientY; + const dx = joystick.endX - joystick.startX; + const dy = joystick.endY - joystick.startY; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance > 0) { + const maxJoystickDistance = 40; + const scale = Math.min(1, distance / maxJoystickDistance); + player.velocityX = (dx / distance) * player.maxSpeed * scale; + player.velocityY = (dy / distance) * player.maxSpeed * scale; + } + } else if (touch.identifier === aimTouch?.identifier) { + // Aiming and shooting + shootingJoystick.currentX = touch.clientX; + shootingJoystick.currentY = touch.clientY; + updatePlayerAim(touch.clientX, touch.clientY); + } + } +} + +function handleTouchEnd(e) { + e.preventDefault(); + if (!gameStarted || !player) return; + + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + if (touch.identifier === moveTouch?.identifier) { + joystick.active = false; + player.velocityX = 0; + player.velocityY = 0; + moveTouch = null; + } else if (touch.identifier === aimTouch?.identifier) { + shootingJoystick.active = false; + aimTouch = null; + player.isShooting = false; + } + } +} + +function updateAndDrawShootingJoystick(deltaTime) { + if (!shootingJoystick.active) { + shootingJoystick.innerRadius = 0; + return; + } + + // Grow the inner circle + shootingJoystick.innerRadius += shootingJoystick.growthRate * deltaTime; + shootingJoystick.innerRadius = Math.min(shootingJoystick.innerRadius, shootingJoystick.maxInnerRadius); + + // Check if we should start shooting + if (shootingJoystick.innerRadius >= shootingJoystick.maxInnerRadius * shootingJoystick.shootThreshold) { + player.isShooting = true; + } + + // Calculate the position of the inner circle + const dx = shootingJoystick.currentX - shootingJoystick.startX; + const dy = shootingJoystick.currentY - shootingJoystick.startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const cappedDistance = Math.min(distance, shootingJoystick.outerRadius); + const innerX = shootingJoystick.startX + Math.cos(angle) * cappedDistance; + const innerY = shootingJoystick.startY + Math.sin(angle) * cappedDistance; + + // Draw the outer circle (base of the joystick) + ctx.beginPath(); + ctx.arc(shootingJoystick.startX, shootingJoystick.startY, shootingJoystick.outerRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 0, 0, 0.2)'; + ctx.fill(); + ctx.closePath(); + + // Draw the inner circle (movable part of the joystick) + ctx.beginPath(); + ctx.arc(innerX, innerY, shootingJoystick.innerRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.fill(); + ctx.closePath(); + + // Draw the aiming line + ctx.beginPath(); + ctx.moveTo(shootingJoystick.startX, shootingJoystick.startY); + ctx.lineTo(innerX, innerY); + ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; + ctx.lineWidth = 2; + ctx.stroke(); +} + + + + + +function updatePlayerAim(clientX, clientY) { + if (!isAutoFireEnabled && player) { + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + if (isMobile && shootingJoystick.active) { + // Mobile touch aiming + const dx = shootingJoystick.currentX - shootingJoystick.startX; + const dy = shootingJoystick.currentY - shootingJoystick.startY; + shootingJoystick.aimAngle = Math.atan2(dy, dx); + player.setWeaponAngle(shootingJoystick.aimAngle); + } else { + // Mouse aiming (for PC) + const rect = canvas.getBoundingClientRect(); + const mouseX = clientX - rect.left + camera.x; + const mouseY = clientY - rect.top + camera.y; + player.mouseX = mouseX; + player.mouseY = mouseY; + const dx = mouseX - player.x; + const dy = mouseY - player.y; + const angle = Math.atan2(dy, dx); + player.setWeaponAngle(angle); + } + } +} + + + +window.addEventListener('keydown', handleKeyDown); +window.addEventListener('keyup', handleKeyUp); +canvas.addEventListener('touchstart', handleTouchStart); +canvas.addEventListener('touchmove', handleTouchMove); +canvas.addEventListener('touchend', handleTouchEnd); + + +document.addEventListener('visibilitychange', function() { + if (!gameStarted || !player) return; // Only proceed if the game is running + + if (document.hidden) { + wasPausedBeforeHidden = isPlayerPaused; + if (!isPlayerPaused) { + pausePlayer(); + socket.emit('player_paused', { isPaused: true }); + } + } else { + if (!wasPausedBeforeHidden && !areInterfacesOpen()) { + unpausePlayer(); + socket.emit('player_paused', { isPaused: false }); + } + } +}); + + +// Modify the play button event listener +playButton.addEventListener('click', () => { + if (socket && socket.connected) { + // If socket is already connected, just try to join + socket.emit('join', { room: 'main_game_room', name: currentUser || `Guest-${Math.floor(Math.random() * 10000)}`, weapon: currentWeapon }); + } else { + startGame(); + } + update(); +}); + +quitButton.addEventListener('click', () => { + window.location.reload(); + }); + +// Update the window resize event handler +window.addEventListener('resize', () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + backgroundCanvas.width = canvas.width; + backgroundCanvas.height = canvas.height; + backgroundCtx.fillStyle = 'rgb(18, 4, 88)'; + backgroundCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height); + if (camera) { + camera.width = canvas.width; + camera.height = canvas.height; + } + if (gameStarted) { + drawExperienceBar(); + updateSynthCoinsDisplay(); + } +}); + +window.addEventListener('load', () => { + const shopButton = document.getElementById('shopButton'); + if (shopButton) { + shopButton.addEventListener('click', toggleShop); + } +}); + + +document.addEventListener('DOMContentLoaded', initShop); + +class Particle { + constructor(x, y, color, radius, velocityX, velocityY, ttl) { + this.x = x; + this.y = y; + this.color = color; + this.radius = radius; + this.velocityX = velocityX; + this.velocityY = velocityY; + this.ttl = ttl; + } + + update() { + this.x += this.velocityX; + this.y += this.velocityY; + this.ttl--; + this.radius *= 0.98; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + } +} + +class ImpactEffect { + constructor(x, y, color, size, angle) { + this.x = x; + this.y = y; + this.color = color; + this.size = size; + this.angle = angle; + this.lifetime = 20; // Number of frames the effect will last + this.particles = []; + this.generateParticles(); + } + + generateParticles() { + const particleCount = 10; + for (let i = 0; i < particleCount; i++) { + const speed = Math.random() * 2 + 1; + const angle = this.angle + (Math.random() - 0.5) * Math.PI / 2; // Spread particles in a cone + this.particles.push({ + x: this.x, + y: this.y, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + size: Math.random() * this.size / 2, + color: this.color + }); + } + } + + update() { + this.lifetime--; + this.particles.forEach(particle => { + particle.x += particle.vx; + particle.y += particle.vy; + particle.size *= 0.9; // Shrink particles over time + }); + } + + draw(ctx) { + this.particles.forEach(particle => { + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); + ctx.fillStyle = particle.color; + ctx.fill(); + }); + } +} + + + +init(); \ No newline at end of file diff --git a/static/js/security.js b/static/js/security.js new file mode 100644 index 0000000..35d082b --- /dev/null +++ b/static/js/security.js @@ -0,0 +1,30 @@ +// Add this to your game.js file or create a new security.js file + +// Disable context menu (right-click menu) +document.addEventListener('contextmenu', function(e) { + e.preventDefault(); +}, false); + +// Disable image dragging +document.addEventListener('dragstart', function(e) { + if (e.target.tagName.toLowerCase() === 'img') { + e.preventDefault(); + } +}, false); + +// Prevent text selection +document.body.style.userSelect = 'none'; +document.body.style.webkitUserSelect = 'none'; +document.body.style.msUserSelect = 'none'; +document.body.style.mozUserSelect = 'none'; + +// Disable touch callout on mobile devices +document.body.style.webkitTouchCallout = 'none'; + +// Optional: Disable saving images on mobile +document.body.style.webkitTapHighlightColor = 'rgba(0,0,0,0)'; + +// Prevent copying text +document.body.oncopy = function(e) { + e.preventDefault(); +}; \ No newline at end of file diff --git a/static/js/skinselector.js b/static/js/skinselector.js new file mode 100644 index 0000000..73f127e --- /dev/null +++ b/static/js/skinselector.js @@ -0,0 +1,267 @@ +const SKIN_CACHE_KEY = 'cachedSkins'; +const SELECTED_SKINS_CACHE_KEY = 'cachedSelectedSkins'; +const SKIN_CACHE_TIMESTAMP_KEY = 'skinCacheTimestamp'; +const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +let loadSkinsDebounceTimer; + + + +function loadSkinOptions() { + if (!currentUser) { + displayNoSkinsMessage("Please sign in to view and select skins."); + return; + } + + clearTimeout(loadSkinsDebounceTimer); + + loadSkinsDebounceTimer = setTimeout(() => { + const cachedSkins = localStorage.getItem(SKIN_CACHE_KEY); + const cachedSelectedSkins = localStorage.getItem(SELECTED_SKINS_CACHE_KEY); + const cacheTimestamp = localStorage.getItem(SKIN_CACHE_TIMESTAMP_KEY); + const currentTime = Date.now(); + + if (cachedSkins && cachedSelectedSkins && cacheTimestamp && (currentTime - parseInt(cacheTimestamp) < CACHE_DURATION)) { + console.log('Using cached skins and selected skins'); + displayUnlockedSkins(JSON.parse(cachedSkins), JSON.parse(cachedSelectedSkins)); + } else { + fetchAndCacheSkins(); + } + }, 300); +} + +function fetchAndCacheSkins() { + const skinCustomization = document.getElementById('skinCustomization'); + if (skinCustomization) { + skinCustomization.innerHTML = '
Loading skins...
'; + } + + const cachedEtag = localStorage.getItem('skinCacheEtag'); + const headers = { + 'Content-Type': 'application/json', + }; + if (cachedEtag) { + headers['If-None-Match'] = cachedEtag; + } + + Promise.all([ + fetch('/get-unlocked-skins', { + method: 'POST', + headers: headers, + body: JSON.stringify({ username: currentUser }), + }), + fetch('/get-player-skin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: currentUser }), + }) + ]) + .then(([unlockedResponse, selectedResponse]) => { + return Promise.all([ + handleUnlockedSkinsResponse(unlockedResponse), + handleSelectedSkinsResponse(selectedResponse) + ]); + }) + .then(([unlockedSkins, selectedSkins]) => { + displayUnlockedSkins(unlockedSkins, selectedSkins); + }) + .catch(error => { + console.error('Error loading skin options:', error); + displayNoSkinsMessage("An error occurred while loading skins. Please try again later."); + }); +} + +function handleUnlockedSkinsResponse(response) { + if (response.status === 304) { + console.log('Server returned 304 for unlocked skins, using cached data'); + return JSON.parse(localStorage.getItem(SKIN_CACHE_KEY)); + } + if (!response.ok) { + throw new Error('Network response was not ok for unlocked skins'); + } + const newEtag = response.headers.get('ETag'); + if (newEtag) { + localStorage.setItem('skinCacheEtag', newEtag); + } + return response.json().then(data => { + localStorage.setItem(SKIN_CACHE_KEY, JSON.stringify(data.skins)); + localStorage.setItem(SKIN_CACHE_TIMESTAMP_KEY, Date.now().toString()); + return data.skins; + }); +} + +function handleSelectedSkinsResponse(response) { + if (!response.ok) { + throw new Error('Network response was not ok for selected skins'); + } + return response.json().then(data => { + localStorage.setItem(SELECTED_SKINS_CACHE_KEY, JSON.stringify(data)); + return data; + }); +} + + +function selectSkin(skinType, skinId, element) { + if (!currentUser) { + console.log('No user logged in'); + return; + } + + fetch(`/select-skin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: currentUser, skin_type: skinType, skin_id: skinId }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.success) { + console.log('Skin selected successfully'); + + // Remove 'selected' class from all skin options of the same type + const options = document.querySelectorAll(`#${skinType}Options .skin-option`); + options.forEach(option => option.classList.remove('selected')); + + // Add 'selected' class to the clicked element + element.classList.add('selected'); + + // Update the local cache + updateLocalSelectedSkinCache(skinType, { + name: element.getAttribute('title').split(' (')[0], + value: skinType === 'color' ? element.style.backgroundColor : element.style.backgroundImage, + effect: element.classList.contains('glow-effect') ? 'glow' : null + }); + + loadPlayerSkin(currentUser); + } else { + console.error('Failed to select skin:', data.error); + } + }) + .catch(error => { + console.error('Error selecting skin:', error); + }); +} + +function updateLocalSelectedSkinCache(skinType, skinData) { + const cachedSelectedSkins = JSON.parse(localStorage.getItem(SELECTED_SKINS_CACHE_KEY) || '{}'); + cachedSelectedSkins[skinType] = skinData; + localStorage.setItem(SELECTED_SKINS_CACHE_KEY, JSON.stringify(cachedSelectedSkins)); +} + + +function updateSkinCache(skinType, skinId) { + const cachedSkins = JSON.parse(localStorage.getItem(SKIN_CACHE_KEY) || '[]'); + const updatedSkins = cachedSkins.map(skin => { + if (skin.type === skinType) { + return { ...skin, id: skinId }; + } + return skin; + }); + localStorage.setItem(SKIN_CACHE_KEY, JSON.stringify(updatedSkins)); +} + + +function displayUnlockedSkins(skins, selectedSkins) { + const skinCustomization = document.getElementById('skinCustomization'); + if (!skinCustomization) { + console.error('skinCustomization element not found'); + return; + } + + // Create a document fragment to build the new content + const fragment = document.createDocumentFragment(); + + const colorSelector = document.createElement('div'); + colorSelector.id = 'colorSelector'; + colorSelector.className = 'skin-section'; + colorSelector.innerHTML = '

Colors

'; + fragment.appendChild(colorSelector); + + const hatSelector = document.createElement('div'); + hatSelector.id = 'hatSelector'; + hatSelector.className = 'skin-section'; + hatSelector.innerHTML = '

Hats

'; + fragment.appendChild(hatSelector); + + // Append the fragment to skinCustomization + skinCustomization.innerHTML = ''; + skinCustomization.appendChild(fragment); + + const colorOptions = document.getElementById('colorOptions'); + const hatOptions = document.getElementById('hatOptions'); + + let hasColors = false; + let hasHats = false; + + skins.forEach(skin => { + const optionElement = document.createElement('div'); + optionElement.classList.add('skin-option'); + optionElement.setAttribute('data-skin-id', skin.id); + optionElement.setAttribute('title', `${skin.name} (${skin.rarity})`); + + if (selectedSkins[skin.type] && selectedSkins[skin.type].name === skin.name) { + optionElement.classList.add('selected'); + } + + switch (skin.type) { + case 'color': + optionElement.style.backgroundColor = skin.value; + if (skin.effect === 'glow') { + optionElement.classList.add('glow-effect'); + } + colorOptions.appendChild(optionElement); + hasColors = true; + break; + case 'hat': + optionElement.style.backgroundImage = `url(${skin.value})`; + hatOptions.appendChild(optionElement); + hasHats = true; + break; + } + + optionElement.addEventListener('click', () => selectSkin(skin.type, skin.id, optionElement)); + }); + + if (selectedSkins.color && !hasColors) { + const defaultColorElement = document.createElement('div'); + defaultColorElement.classList.add('skin-option', 'selected'); + defaultColorElement.style.backgroundColor = selectedSkins.color.value; + defaultColorElement.setAttribute('title', selectedSkins.color.name); + colorOptions.appendChild(defaultColorElement); + hasColors = true; + } + + if (!hasColors) colorOptions.innerHTML = '

No colors unlocked yet

'; + if (!hasHats) hatOptions.innerHTML = '

No hats unlocked yet

'; + + // Fade in the skin sections + setTimeout(() => { + document.getElementById('colorSelector').classList.add('loaded'); + document.getElementById('hatSelector').classList.add('loaded'); + }, 50); + + if (!hasColors && !hasHats) { + displayNoSkinsMessage("You haven't unlocked any skins yet. Keep playing to earn skins!"); + } +} + + +function displayNoSkinsMessage(message) { + const skinCustomization = document.getElementById('skinCustomization'); + if (skinCustomization) { + skinCustomization.innerHTML = `

${message}

`; + } else { + console.error('skinCustomization element not found'); + } +} + + +// Call this function when the customization tab is opened +function onCustomizationTabOpen() { + loadSkinOptions(); +} \ No newline at end of file diff --git a/static/site.webmanifest b/static/site.webmanifest new file mode 100644 index 0000000..7058cec --- /dev/null +++ b/static/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Resonance Rumble", + "short_name": "ResRumble", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#00ffff", + "background_color": "#120458", + "display": "standalone" + } \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..3c87409 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,1783 @@ +/* Resonance Rumble Styles */ + +body { + margin: 0; + overflow: hidden; + background-color: #120458; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-family: 'Orbitron', sans-serif; + color: #00ffff; +} + +#gameCanvas { + display: block; +} + +#ui { + position: fixed; + top: 10px; + left: 10px; + display: flex; + gap: 10px; + z-index: 1000; +} + +#levelUpPopup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 30px; + border-radius: 10px; + text-align: center; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + font-family: 'Orbitron', sans-serif; + animation: popIn 0.5s ease-out; +} + +@keyframes popIn { + 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; } + 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } +} + +#levelUpPopup h2 { + font-size: 36px; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + +#levelUpPopup p { + font-size: 18px; + margin-bottom: 20px; +} + +#upgradeOptions { + display: flex; + flex-direction: column; + gap: 15px; +} + +.upgrade-button, .upgrade-option { + background-color: #ff00ff; + color: white; + border: none; + padding: 15px; + + font-family: 'Orbitron', sans-serif; + font-size: 18px; + text-transform: uppercase; + letter-spacing: 2px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + display: flex; + justify-content: space-between; + align-items: center; +} + +.upgrade-button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.upgrade-name { + flex-grow: 1; + text-align: left; +} + +.upgrade-value { + background-color: rgba(255, 255, 255, 0.2); + padding: 5px 10px; + border-radius: 5px; + font-weight: bold; +} + +.fade-out { + animation: fadeOut 0.5s ease-in forwards; +} + +@keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + + +#gameControls { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + background-color: rgba(18, 4, 88, 0.8); + padding: 30px; + border-radius: 10px; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; +} + +.button { + background-color: #ff00ff; + color: white; + padding: 12px 24px; + border: none; + + font-size: 18px; + font-family: 'Orbitron', sans-serif; + text-transform: uppercase; + letter-spacing: 2px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; +} + +.button::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transform: rotate(45deg); + transition: all 0.3s ease; +} + +.button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.button:hover::before { + left: 150%; +} + +#startMessage { + color: #00ffff; + font-size: 24px; + text-align: center; + text-shadow: 0 0 10px #00ffff; +} + +#uiWidget { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 20px; + border-radius: 10px; + text-align: center; + transition: all 0.3s ease; + opacity: 0; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + pointer-events: none; + width: 80%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; +} + +#uiWidget.visible { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + pointer-events: auto; +} + +#account { + text-align: center; + padding: 20px; +} + +#accountContent { + margin-top: 20px; +} + +.widget-tabs { + display: flex; + justify-content: space-around; + margin-bottom: 20px; +} + +.tab-button { + background-color: #ff00ff; + color: white; + border: none; + padding: 10px 20px; + + font-family: 'Orbitron', sans-serif; + font-size: 16px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + position: relative; + overflow: hidden; +} + +.tab-button::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transform: rotate(45deg); + transition: all 0.3s ease; +} + + +.tab-button:hover, .tab-button.active { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.tab-button:hover::before, .tab-button.active::before { + left: 150%; +} + +.widget-content { + text-align: left; + padding-bottom: 20px; + background-color: transparent; /* Remove background color */ + border-radius: 10px; + box-shadow: none; /* Remove box shadow */ + padding: 20px; + margin-bottom: 20px; +} + + +.tab-content { + display: none; + flex-direction: column; + height: 100%; +} + +.tab-content.active { + display: flex; +} + + +#uiWidget, #classWidget, #infoWidget { + /* ... other styles ... */ + display: none; +} + +#uiWidget.visible, #classWidget.visible, #infoWidget.visible { + display: block; +} + +.close-button { + background-color: #ff00ff; + color: white; + border: none; + padding: 10px 20px; + + font-family: 'Orbitron', sans-serif; + font-size: 16px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + margin-top: 20px; + align-self: center; +} + +.close-button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +#weaponSelector { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 15px; +} + +.weapon-option { + width: 80px; + height: 80px; + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 10px; + padding: 10px; + + transition: all 0.3s ease; + display: flex; + justify-content: center; + align-items: center; +} + +.weapon-option:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.weapon-option img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + + +.weapon-option.selected { + background-color: rgba(0, 255, 255, 0.2); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +#buyWeaponButton { + background-color: #ff00ff; + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; + font-family: 'Orbitron', sans-serif; + + border-radius: 5px; + transition: all 0.3s ease; + margin-top: 15px; +} + +#buyWeaponButton:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + + + +.weapon-button { + background-color: #ff00ff; + color: white; + padding: 10px 20px; + border: none; + + font-family: 'Orbitron', sans-serif; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + border-radius: 5px; + transition: all 0.3s ease; + box-shadow: 0 0 5px #ff00ff; +} + +.weapon-button:hover { + transform: scale(1.05); + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; +} + +.weapon-button.selected { + background-color: #00ffff; + color: #120458; + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + +#authButtons { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.auth-form { + display: none; + flex-direction: column; + gap: 10px; + margin-top: 20px; +} + +.auth-form input { + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + color: #00ffff; + padding: 12px; + font-size: 16px; + width: 200px; + text-align: center; + font-family: 'Orbitron', sans-serif; + border-radius: 5px; + transition: all 0.3s ease; +} + +.auth-form input:focus { + outline: none; + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + +.auth-form input.valid { + box-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; +} + +.auth-form input.invalid { + box-shadow: 0 0 10px #ff0000, 0 0 20px #ff0000; +} + +/* Add this to your existing CSS */ +.synth-coins-counter { + position: absolute; + top: 10px; + left: 10px; + background-color: rgba(0, 0, 0, 0.5); + color: #FFD700; + padding: 5px 10px; + border-radius: 5px; + font-family: 'Orbitron', sans-serif; + font-size: 16px; +} + + +#logoutButton { + display: none; + margin-bottom: 20px; +} + +#welcomeMessage { + color: #00ffff; + font-size: 24px; + text-align: center; + text-shadow: 0 0 10px #00ffff; + margin: 0; +} + +#playerName { + display: none; +} + +#skinCustomization { + margin-top: 20px; +} + +.skin-section { + opacity: 1; + display: block; +} + +.loading-spinner { + display: flex; + justify-content: center; + align-items: center; + height: 100px; + font-size: 18px; + color: #fff; +} + + + +.skin-section h4 { + margin-bottom: 10px; +} + +#colorOptions, #hatOptions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.skin-option { + position: relative; + width: 60px; + height: 60px; + margin: 5px; + + border: 2px solid #00ffff; + border-radius: 10px; + transition: all 0.3s ease; +} + +.skin-option:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.skin-option.selected { + border-color: #ff00ff; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; +} + +.skin-option.selected::after { + content: '✓'; + position: absolute; + top: 5px; + right: 5px; + color: #ff00ff; + font-size: 20px; + font-weight: bold; +} + +#colorOptions .skin-option { + border-radius: 50%; +} + +#hatOptions .skin-option { + background-size: cover; + background-position: center; +} + +.skin-option.glow-effect { + animation: glow 1.5s ease-in-out infinite alternate; +} + +@keyframes glow { + from { + box-shadow: 0 0 5px 2px currentColor; + } + to { + box-shadow: 0 0 15px 5px currentColor; + } +} + +.skin-option.glow-effect { + box-shadow: 0 0 10px 3px currentColor; + animation: glow 1.5s ease-in-out infinite alternate; +} + +.no-skins-message { + text-align: center; + color: #00ffff; + font-size: 16px; + margin-top: 20px; + padding: 10px; + background-color: rgba(0, 255, 255, 0.1); + border-radius: 5px; + box-shadow: 0 0 10px #00ffff; +} + +.sign-in-message { + text-align: center; + color: #00ffff; + font-size: 16px; + margin-top: 20px; + padding: 10px; + background-color: rgba(0, 255, 255, 0.1); + border-radius: 5px; + box-shadow: 0 0 10px #00ffff; +} + + +#minimap-container { + position: fixed; + right: 10px; + bottom: 10px; + z-index: 1000; + } + + #minimap { + display: none; + border: 2px solid #00ffff; + background-color: rgba(18, 4, 88, 0.7); + border-radius: 10px; + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; + } + + #toggle-minimap { + position: absolute; + right: 0; + bottom: 0; + background-color: #ff00ff; + color: white; + border: none; + padding: 8px 16px; + + font-family: 'Orbitron', sans-serif; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + overflow: hidden; + } + + #toggle-minimap::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transform: rotate(45deg); + transition: all 0.3s ease; + } + + + #toggle-minimap:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; + } + + #toggle-minimap:hover::before { + left: 150%; + } + + + + #minimap.small { + display: block; + width: 150px; + height: 150px; + } + + #minimap.large { + display: block; + position: fixed; + width: 80%; + height: 80%; + top: 10%; + left: 10%; + z-index: 1001; + } + + + +#infoWidget { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 20px; + border-radius: 10px; + text-align: center; + transition: all 0.3s ease; + opacity: 0; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + pointer-events: none; + width: 80%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; +} + +#infoWidget.visible { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + pointer-events: auto; +} + +.social-links { + display: flex; + flex-direction: column; + gap: 15px; + margin-top: 20px; + margin-bottom: 10px; /* Add margin to the bottom */ + padding: 10px; + background-color: rgba(18, 4, 88, 0.6); + border-radius: 10px; + box-shadow: 0 0 10px #ff00ff, inset 0 0 5px #00ffff; +} + +.social-link { + display: flex; + align-items: center; + padding: 12px 20px; + background-color: rgba(255, 0, 255, 0.1); + border: 1px solid #ff00ff; + border-radius: 5px; + color: #00ffff; + text-decoration: none; + font-size: 18px; + letter-spacing: 1px; + transition: all 0.3s ease; +} + +.social-link:hover { + background-color: rgba(0, 255, 255, 0.1); + border-color: #00ffff; + box-shadow: 0 0 15px #00ffff; + transform: translateX(5px); +} + +.social-link i { + margin-right: 15px; + font-size: 24px; + width: 24px; + text-align: center; + color: #ff00ff; + transition: all 0.3s ease; +} + +.social-link:hover i { + color: #00ffff; + transform: scale(1.2); +} + +/* Update for both uiWidget and infoWidget */ +#uiWidget .widget-content, +#infoWidget .widget-content { + background-color: transparent; + box-shadow: none; +} + +/* Remove any specific styling for social-links container if it exists */ +.social-links { + background-color: transparent; + box-shadow: none; +} + +/* Ensure tab content is visible without the container styling */ +.tab-content { + color: #00ffff; /* Ensure text is visible against the widget background */ +} + +/* Adjust spacing as needed */ +.close-button { + margin-top: 10px; /* Reduce top margin slightly */ +} + +#classSelectorButton { + background-color: #ff00ff; + color: white; + padding: 12px 24px; + border: none; + + font-size: 18px; + font-family: 'Orbitron', sans-serif; + text-transform: uppercase; + letter-spacing: 2px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + margin-bottom: 20px; +} + +#classSelectorButton:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.class-option:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.class-option img { + width: 100px; + height: 100px; + object-fit: contain; + margin-bottom: 10px; +} + +.class-option h3 { + color: #00ffff; + margin-bottom: 10px; +} + +.class-option p { + color: white; + font-size: 14px; + margin: 5px 0; +} + +.class-option.locked { + opacity: 0.5; + cursor: not-allowed; +} + +.lock-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; +} + +.lock-overlay i { + font-size: 48px; + color: #ff00ff; +} + +.requirements-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 30px; + border-radius: 10px; + text-align: center; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; +} + +.requirements-popup h3 { + margin-bottom: 20px; +} + +.requirements-popup ul { + list-style-type: none; + padding: 0; + margin-bottom: 20px; +} + +.requirements-popup li { + margin-bottom: 10px; +} + +.requirements-popup button { + background-color: #ff00ff; + color: white; + padding: 10px 20px; + border: none; + + font-family: 'Orbitron', sans-serif; + font-size: 16px; + transition: all 0.3s ease; + border-radius: 5px; +} + +.requirements-popup button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); +} + +#classWidget { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 20px; + border-radius: 10px; + text-align: center; + transition: all 0.3s ease; + opacity: 0; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + pointer-events: none; + width: 80%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; +} + +#classWidget.visible { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + pointer-events: auto; +} + +#classInfoBox { + display: flex; + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; +} + +#classImage { + width: 100px; + height: 100px; + margin-right: 20px; +} + +#classImage img { + width: 100%; + height: 100%; + object-fit: contain; +} + +#classDetails { + flex-grow: 1; +} + +#className { + color: #00ffff; + margin-bottom: 10px; + font-size: 24px; +} + +#classStats p { + color: white; + font-size: 14px; + margin: 5px 0; +} + + +#classSelector { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 15px; +} + +.class-option { + width: 80px; + height: 80px; + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 10px; + padding: 10px; + + transition: all 0.3s ease; + position: relative; +} + +.class-option:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + + +.class-option img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.class-option h3 { + color: #00ffff; + margin-bottom: 5px; + font-size: 16px; +} + +.class-option p { + color: white; + font-size: 12px; + margin: 3px 0; +} + +.class-option.selected { + background-color: rgba(0, 255, 255, 0.2); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.class-option.selected::after { + content: '✓'; + position: absolute; + top: 5px; + right: 5px; + color: #00ffff; + font-size: 20px; + font-weight: bold; +} + +.class-option.locked { + opacity: 0.5; + cursor: not-allowed; +} + +#className .locked-indicator { + font-size: 0.8em; + color: #ff00ff; + margin-left: 10px; +} + +#experienceBar { + position: fixed; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + width: 80%; + max-width: 400px; + height: 20px; + z-index: 1000; +} + +#synthCoinsCounter { + position: fixed; + bottom: 50px; /* Positioned above the shop button */ + left: 10px; + z-index: 1000; +} + + + +.lock-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; +} + +.lock-overlay i { + font-size: 24px; + color: #ff00ff; +} + +/* Add these styles to your existing styles.css file */ + +#shopButton { + position: fixed; + bottom: 10px; /* Position at the bottom */ + left: 10px; /* Position on the left */ + background-color: #ff00ff; + color: white; + border: none; + padding: 8px 16px; + + font-family: 'Orbitron', sans-serif; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; + overflow: hidden; + z-index: 1000; +} + + +#shopButton::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transform: rotate(45deg); + transition: all 0.3s ease; +} + +#shopButton:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +#shopButton:hover::before { + left: 150%; +} + +#shopWidget { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 20px; + border-radius: 10px; + text-align: center; + transition: all 0.3s ease; + opacity: 0; + z-index: 1000; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + pointer-events: none; + width: 80%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; + display: none; +} + +#shopWidget.visible { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + pointer-events: auto; + display: block; +} + +#shopWidget .tab-content { + display: none; +} + +#shopWidget .tab-content.active { + display: block; +} + + +#weaponInfoBox { + display: flex; + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; +} + +#weaponImage { + width: 100px; + height: 100px; + margin-right: 20px; +} + +#weaponImage img { + width: 100%; + height: 100%; + object-fit: contain; +} + +#weaponDetails { + flex-grow: 1; + text-align: left; +} + +#weaponName { + color: #00ffff; + margin-bottom: 10px; + font-size: 24px; +} + +#weaponStats p { + color: white; + font-size: 14px; + margin: 5px 0; +} + + +#shopWidget h2 { + font-size: 24px; + margin-bottom: 20px; + text-shadow: 0 0 10px #00ffff; +} + +#shopItems { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + gap: 20px; +} + +.shop-item { + background-color: rgba(255, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 10px; + padding: 15px; + width: 200px; + transition: all 0.3s ease; +} + +.shop-item:hover { + transform: scale(1.05); + box-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; +} + +.shop-item img { + width: 100px; + height: 100px; + object-fit: contain; + margin-bottom: 10px; +} + +.shop-item h3 { + font-size: 18px; + margin-bottom: 10px; +} + +.shop-item p { + font-size: 14px; + margin-bottom: 15px; +} + +.buy-button { + background-color: #ff00ff; + color: white; + border: none; + padding: 8px 16px; + font-size: 14px; + font-family: 'Orbitron', sans-serif; + + border-radius: 5px; + transition: all 0.3s ease; +} + +.buy-button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + +.hidden { + display: none; +} + +.setting-option { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +#debugCheckbox, #autoFireCheckbox { + appearance: none; + width: 20px; + height: 20px; + border: 2px solid #00ffff; + border-radius: 4px; + background-color: transparent; + + margin-right: 10px; +} + +#debugCheckbox:checked, #autoFireCheckbox:checked { + background-color: #00ffff; + position: relative; +} + +#debugCheckbox:checked::before, #autoFireCheckbox:checked::before { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #120458; + font-size: 14px; + font-weight: bold; +} + +.draggable { + cursor: move; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + .draggable * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + +.setting-option label { + color: #00ffff; + font-family: 'Orbitron', sans-serif; + font-size: 16px; + +} + +#notificationContainer { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 2000; +} + +.custom-notification { + background-color: rgba(18, 4, 88, 0.9); + color: #00ffff; + padding: 20px; + border-radius: 10px; + text-align: center; + box-shadow: 0 0 20px #ff00ff, 0 0 40px #ff00ff; + border: 2px solid #ff00ff; + font-family: 'Orbitron', sans-serif; + max-width: 300px; + margin-bottom: 10px; + opacity: 0; + transform: translateY(-20px); + transition: opacity 0.3s, transform 0.3s; +} + +.custom-notification.show { + opacity: 1; + transform: translateY(0); +} + +.custom-notification .notification-content { + display: flex; + flex-direction: column; + align-items: center; +} + +.custom-notification p { + margin-bottom: 15px; +} + +.custom-notification .close-notification { + background-color: #ff00ff; + color: white; + border: none; + padding: 8px 16px; + + font-family: 'Orbitron', sans-serif; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.3s ease; + border-radius: 5px; +} + +.custom-notification .close-notification:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + +body { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +img { + -webkit-user-drag: none; + -khtml-user-drag: none; + -moz-user-drag: none; + -o-user-drag: none; + pointer-events: none; +} + +.discord-info { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 20px; + background-color: rgba(0, 255, 255, 0.1); + padding: 10px; + border-radius: 10px; +} + +.discord-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + border: 2px solid #00ffff; +} + +.discord-username { + flex-grow: 1; + margin-left: 15px; + font-size: 16px; + color: #00ffff; +} + + + +.discord-details { + margin-left: 20px; +} + +.small-button { + background-color: #ff00ff; + color: white; + border: none; + padding: 5px 10px; + font-size: 12px; + font-family: 'Orbitron', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.3s ease; + border-radius: 5px; + box-shadow: 0 0 5px #ff00ff; +} + +.small-button:hover { + background-color: #00ffff; + color: #120458; + transform: scale(1.05); + box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; +} + + + + +/* Custom Cursor Styles */ +:root { + --cursor-color: #ff00ff; /* Neon pink */ + --cursor-outline: #00ffff; /* Cyan outline */ + } + + body { + cursor: none; + } + + .custom-cursor { + position: fixed; + width: 20px; + height: 20px; + pointer-events: none; + z-index: 9999; + mix-blend-mode: difference; /* This will help the cursor stand out on different backgrounds */ + } + + .cursor-dot { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 4px; + height: 4px; + background-color: var(--cursor-color); + border-radius: 50%; + box-shadow: 0 0 5px var(--cursor-color), 0 0 10px var(--cursor-color); + } + + .cursor-outline { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 16px; + height: 16px; + border: 2px solid var(--cursor-outline); + border-radius: 50%; + transition: all 0.1s ease-out; + } + + .custom-cursor.clicking .cursor-outline { + width: 12px; + height: 12px; + } + + /* Style for interactive elements */ + button, .draggable, .class-option, .weapon-option { + cursor: none; + } + + button:hover, .draggable:hover, .class-option:hover, .weapon-option:hover { + cursor: none; + } + + +/* Hide custom cursor for mobile devices */ +@media (hover: none) and (pointer: coarse) { + body { + cursor: auto; + } + + .custom-cursor { + display: none; + } + } + + +@media screen and (max-width: 600px) { + + #weaponSelector { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + + #weaponInfoBox { + flex-direction: column; + align-items: center; + } + + #weaponImage { + margin-right: 0; + margin-bottom: 10px; + } + + #weaponDetails { + text-align: center; + } + + .weapon-option { + width: 60px; + height: 60px; + padding: 5px; + } + + #synthCoinsCounter { + bottom: 40px; /* Slightly closer to the shop button on smaller screens */ + } + + #shopWidget { + max-width: 95%; + } + + .shop-item { + width: 150px; + } + + .shop-item img { + width: 80px; + height: 80px; + } + + .shop-item h3 { + font-size: 16px; + } + + .shop-item p { + font-size: 12px; + } + + .buy-button { + padding: 6px 12px; + font-size: 12px; + } +} + +@media screen and (max-width: 600px) { + + #experienceBar { + position: fixed; + bottom: 10px; + left: 70px; + width: calc(100% - 140px); + max-width: none; + transform: none; + z-index: 999; + } + + #experienceBar canvas { + font-size: 10px; /* Adjust this value as needed */ + } + + #levelText { + font-size: 10px; /* Adjust this value as needed */ + } + + + + #classInfoBox { + flex-direction: column; + align-items: center; + } + + #classImage { + margin-right: 0; + margin-bottom: 10px; + } + + #classDetails { + text-align: center; + } + + .class-option { + width: 60px; + height: 60px; + } + + + .social-link { + font-size: 16px; + padding: 10px 15px; + } + + .social-link i { + font-size: 20px; + width: 20px; + margin-right: 10px; + } +} + +@keyframes glow { + from { + box-shadow: 0 0 5px 2px currentColor; + } + to { + box-shadow: 0 0 15px 5px currentColor; + } +} + + +@media (max-width: 600px) { + #gameControls { + width: 90%; + } + + #uiWidget { + width: 95%; + max-height: 95vh; + } +} + +/* Responsive styles for mobile devices */ +@media screen and (max-width: 600px) { + #ui { + top: 5px; + left: 5px; + gap: 5px; + } + + .button { + padding: 8px 16px; + font-size: 14px; + } + + #gameControls { + padding: 20px; + } + + #startMessage { + font-size: 18px; + } + + #uiWidget { + width: 95%; + padding: 15px; + } + + .widget-tabs { + flex-wrap: wrap; + } + + .tab-button { + padding: 8px 16px; + font-size: 14px; + margin-bottom: 5px; + } + + .weapon-button { + padding: 8px 16px; + font-size: 12px; + } + + .auth-form input { + padding: 8px; + font-size: 14px; + width: 100%; + } + + #welcomeMessage { + font-size: 18px; + } + + #minimap.small { + width: 100px; + height: 100px; + } + + #minimap.large { + width: 90%; + height: 90%; + top: 5%; + left: 5%; + } + + #shopButton, + #toggle-minimap { + position: fixed; + bottom: 10px; + z-index: 1000; + } + + #shopButton { + left: 10px; + } + + #toggle-minimap { + right: 10px; + } + + /* Experience bar specific styles */ + #levelUpPopup { + padding: 20px; + width: 90%; + max-width: none; + } + + #levelUpPopup h2 { + font-size: 24px; + margin-bottom: 10px; + } + + #levelUpPopup p { + font-size: 14px; + margin-bottom: 10px; + } + + .upgrade-button, .upgrade-option { + padding: 10px; + font-size: 14px; + } +} + +/* Additional adjustments for very small screens */ +@media screen and (max-width: 320px) { + #ui { + flex-direction: column; + align-items: flex-start; + } + + .button { + padding: 6px 12px; + font-size: 12px; + } + + #startMessage { + font-size: 16px; + } + + #uiWidget { + padding: 10px; + } + + .tab-button { + padding: 6px 12px; + font-size: 12px; + } + + .weapon-button { + padding: 6px 12px; + font-size: 10px; + } + + #welcomeMessage { + font-size: 16px; + } + + #minimap.small { + width: 80px; + height: 80px; + } + + #toggle-minimap { + padding: 4px 8px; + font-size: 10px; + } + + /* Experience bar specific styles */ + #levelUpPopup h2 { + font-size: 20px; + } + + #levelUpPopup p { + font-size: 12px; + } + + .upgrade-button, .upgrade-option { + padding: 8px; + font-size: 12px; + } +} diff --git a/static/weapons/asd.png b/static/weapons/asd.png new file mode 100644 index 0000000..1fada8a Binary files /dev/null and b/static/weapons/asd.png differ diff --git a/static/weapons/basic_blaster - Copy (7).png b/static/weapons/basic_blaster - Copy (7).png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/basic_blaster - Copy (7).png differ diff --git a/static/weapons/basic_blaster - Copy.png b/static/weapons/basic_blaster - Copy.png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/basic_blaster - Copy.png differ diff --git a/static/weapons/basic_blaster.png b/static/weapons/basic_blaster.png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/basic_blaster.png differ diff --git a/static/weapons/glitch_cannon.png b/static/weapons/glitch_cannon.png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/glitch_cannon.png differ diff --git a/static/weapons/neon_blaster.png b/static/weapons/neon_blaster.png new file mode 100644 index 0000000..e2e5758 Binary files /dev/null and b/static/weapons/neon_blaster.png differ diff --git a/static/weapons/neon_blaster.svg b/static/weapons/neon_blaster.svg new file mode 100644 index 0000000..39fe5b0 --- /dev/null +++ b/static/weapons/neon_blaster.svg @@ -0,0 +1,120429 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/weapons/pew_pew.png b/static/weapons/pew_pew.png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/pew_pew.png differ diff --git a/static/weapons/synth_rifle.png b/static/weapons/synth_rifle.png new file mode 100644 index 0000000..edb89cb Binary files /dev/null and b/static/weapons/synth_rifle.png differ diff --git a/styles.txt b/styles.txt new file mode 100644 index 0000000..e69de29