From a0d6aea811f29b543c5a99e51a42086703bc38cc Mon Sep 17 00:00:00 2001 From: Jamon Date: Mon, 12 Aug 2024 15:59:49 +1200 Subject: [PATCH] Fixing achievement notifications --- .gitignore | 1 + game_server.py | 76 +++++++++++++++++++++++++++++++++++++++++++++-- static/js/game.js | 62 +++++++++++++++++++++++++++----------- static/styles.css | 17 +++++++++-- 4 files changed, 134 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 25788bb..50bdcf6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ node-modules/ game-assets/ backup/ update_users.py +updateusers.py skinimporter.py package.json package-lock.json diff --git a/game_server.py b/game_server.py index f5f79f6..536805c 100644 --- a/game_server.py +++ b/game_server.py @@ -43,6 +43,10 @@ app = Flask(__name__) app.config['SECRET_KEY'] = secret_key socketio = SocketIO(app, cors_allowed_origins="*") +socket_to_user_id = {} + + + # MongoDB setup client = MongoClient('mongodb://localhost:27017/') db = client['resonance_rumble'] @@ -56,6 +60,44 @@ active_sockets_collection = db['active_sockets'] oauth_states = db['oauth_states'] achievements_collection = db['achievements'] +def update_user_stat(user_id, stat_name, value, operation='inc'): + if user_id is None: + print("Cannot update stats for guest or unknown user") + return + + update = {} + if operation == 'inc': + update = {'$inc': {f'stats.{stat_name}': value}} + elif operation == 'max': + update = {'$max': {f'stats.{stat_name}': value}} + else: + raise ValueError("Invalid operation") + + result = users_collection.update_one({'_id': user_id}, update) + if result.modified_count > 0: + check_achievements(user_id) + else: + print(f"Failed to update stat for user_id: {user_id}") + +def check_achievements(user_id): + user = users_collection.find_one({'_id': user_id}) + if not user: + print(f"User not found for id: {user_id}") + return + + user_stats = user.get('stats', {}) + + achievements_to_check = [ + {'id': 'kill_10_enemies', 'name': 'Killing spree', 'condition': lambda s: s['enemies_killed'] >= 10}, + {'id': 'kill_100_enemies', 'name': 'Killing frenzy', 'condition': lambda s: s['enemies_killed'] >= 100}, + {'id': 'collect_50_coins', 'name': 'Coin collector', 'condition': lambda s: s['coins_collected'] >= 50}, + {'id': 'reach_level_10', 'name': 'Ding!', 'condition': lambda s: s['highest_level'] >= 10}, + ] + + for achievement in achievements_to_check: + if achievement['id'] not in user['achievements'] and achievement['condition'](user_stats): + award_achievement(user_id, achievement['id']) + def decode_token(token): try: payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) @@ -124,6 +166,20 @@ def award_achievement(user_id, achievement_id): {'$push': {'achievements': achievement_id}} ) + achievement = achievements_collection.find_one({'id': achievement_id}) + if achievement: + # Find the socket ID for this user + player_socket_id = next((sid for sid, player in game_state['players'].items() if player.name == user['username']), None) + + if player_socket_id: + socketio.emit('achievement_unlocked', { + 'name': achievement['name'], + 'description': achievement['description'], + 'image': achievement.get('image', '') + }, room=player_socket_id) + else: + print(f"Could not find socket ID for user {user['username']}") + send_discord_alert(f"🏆 Player {user['username']} has earned the '{achievement['name']}' achievement!", user['username']) # Send a DM to the user if they have a linked Discord account @@ -851,7 +907,14 @@ def signup(): }, "unlocked_skins": [ObjectId("669e1272813db368f3b19519")], # Glowy Blue "unlocked_classes": default_class_ids, - "selected_class": default_class_ids[0] if default_class_ids else None + "selected_class": default_class_ids[0] if default_class_ids else None, + 'achievements': [], + 'stats': { + 'enemies_killed': 0, + 'coins_collected': 0, + 'highest_level': 1, + } + } users_collection.insert_one(new_user) @@ -1025,6 +1088,8 @@ def on_disconnect(): socketio.emit('player_disconnected', player_id, to=main_room) send_discord_alert(f"👋 Player {username} has disconnected!", username) print(f"Player {player_id} disconnected") + if player_id in socket_to_user_id: + del socket_to_user_id[player_id] active_sockets_collection.delete_one({'_id': player_id}) @socketio.on('player_paused') @@ -1053,6 +1118,7 @@ def on_join(data): # Get player skin user = users_collection.find_one({'username': username}) if user: + socket_to_user_id[player_id] = user['_id'] selected_skins = user.get('selected_skins', {}) skin_data = {} for skin_type, skin_id in selected_skins.items(): @@ -1161,6 +1227,8 @@ def on_collect_synth_coin(data): if math.hypot(coin.x - coin_x, coin.y - coin_y) < 0.1: player.synth_coins += 100 game_state['synth_coins'].remove(coin) + if player_id in socket_to_user_id: + update_user_stat(socket_to_user_id[player_id], 'coins_collected', 1) print(f"Player {player.name} collected a coin. New count: {player.synth_coins}") socketio.emit('coin_collected', { 'player_id': player_id, @@ -1303,7 +1371,9 @@ def add_experience(player, amount): player.level += 1 player.experience -= player.max_experience player.max_experience = int(player.max_experience * 1.2) - + # if player_id in socket_to_user_id: + # update_user_stat(socket_to_user_id[player_id], 'highest_level', player.level, operation='max') + # 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) @@ -1458,6 +1528,8 @@ def game_loop(): if enemy['current_health'] <= 0: game_state['enemies'].remove(enemy) + if player_id in socket_to_user_id: + update_user_stat(socket_to_user_id[player_id], 'enemies_killed', 1) coin = SynthCoin(enemy['x'], enemy['y']) game_state['synth_coins'].append(coin) diff --git a/static/js/game.js b/static/js/game.js index 67153fb..41147af 100644 --- a/static/js/game.js +++ b/static/js/game.js @@ -75,10 +75,6 @@ let lastUpdateTime = Date.now(); let impactEffects = []; let currentSongName = ''; - - - - class NotificationSystem { constructor() { this.notificationContainer = null; @@ -91,46 +87,73 @@ class NotificationSystem { document.body.appendChild(this.notificationContainer); } - showNotification(message, image = null) { + showNotification(message, image) { + console.log('showNotification called with message:', message, 'and image:', image); const notificationElement = document.createElement('div'); notificationElement.className = 'custom-notification draggable'; let content = `
`; - if (image) { - content += `Achievement`; + if (image && typeof image === 'string') { + console.log('Adding image to notification:', image); + content += `Achievement`; + } else { + console.log('No valid image provided for notification'); } content += `

${message}

`; + console.log('Notification content:', content); + notificationElement.innerHTML = content; - + const closeButton = notificationElement.querySelector('.close-notification'); closeButton.addEventListener('click', () => this.closeNotification(notificationElement)); - + this.notificationContainer.appendChild(notificationElement); setTimeout(() => notificationElement.classList.add('show'), 10); - + makeDraggable(notificationElement); + + // Add auto-hide after 3 seconds + setTimeout(() => this.closeNotification(notificationElement), 3000); } - - closeNotification(notificationElement) { notificationElement.classList.remove('show'); setTimeout(() => notificationElement.remove(), 300); } } + const notificationSystem = new NotificationSystem(); -function handleAchievementUnlock(data) { - const message = `Achievement Unlocked: ${data.name}\n${data.description}`; - notificationSystem.showNotification(message, data.image); - +function preloadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); } + +function handleAchievementUnlock(data) { + console.log('Achievement unlocked (full data):', JSON.stringify(data)); + if (!notificationSystem) { + console.error('NotificationSystem not initialized'); + return; + } + const message = `Achievement Unlocked: ${data.name}\n${data.description}`; + console.log('Showing notification:', message); + console.log('Achievement image path:', data.image); + console.log('Calling showNotification with message:', message, 'and image:', data.image); + notificationSystem.showNotification(message, data.image); +} + + + function fetchUserAchievements() { const achievementsGrid = document.getElementById('achievementsGrid'); const achievementInfoBox = document.getElementById('achievementInfoBox'); @@ -2199,8 +2222,11 @@ function startGame() { }); - socket.on('achievement_unlocked', handleAchievementUnlock); - + socket.on('achievement_unlocked', (data) => { + console.log('Received achievement_unlocked event:', data); + handleAchievementUnlock(data); + }); + socket.on('reconnect', (attemptNumber) => { debugLog(`Reconnected to server after ${attemptNumber} attempts`); diff --git a/static/styles.css b/static/styles.css index 013723f..04bdd5f 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1460,7 +1460,7 @@ body { top: 20px; left: 50%; transform: translateX(-50%); - z-index: 2000; + z-index: 9999; } .achievements-grid { @@ -1668,6 +1668,12 @@ body { transform: translateY(0); } +.custom-notification.fade-out { + opacity: 0; + transform: translateY(-20px); +} + + .custom-notification .notification-content { display: flex; flex-direction: column; @@ -1683,13 +1689,13 @@ body { 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; + cursor: none; } .custom-notification .close-notification:hover { @@ -1699,6 +1705,13 @@ body { box-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; } +.notification-image { + max-width: 100%; + height: auto; + margin-bottom: 10px; +} + + body { -webkit-user-select: none; -moz-user-select: none;