diff --git a/app/controllers/break_escape/games_controller.rb b/app/controllers/break_escape/games_controller.rb index 7cb9672..56c9378 100644 --- a/app/controllers/break_escape/games_controller.rb +++ b/app/controllers/break_escape/games_controller.rb @@ -2,6 +2,8 @@ require 'open3' module BreakEscape class GamesController < ApplicationController + helper PlayerPreferencesHelper + before_action :set_game, only: [:show, :scenario, :scenario_map, :ink, :room, :container, :sync_state, :update_room, :unlock, :inventory, :objectives, :complete_task, :update_task_progress, :submit_flag] # GET /games/new?mission_id=:id @@ -99,6 +101,15 @@ module BreakEscape def show authorize @game if defined?(Pundit) @mission = @game.mission + + # Load player preference data for the in-game modal + @player_preference = current_player_preference || create_default_preference + @available_sprites = PlayerPreference::AVAILABLE_SPRITES + + # Debug logging + Rails.logger.info "[BreakEscape] Loading game#show for player: #{current_player.class.name}##{current_player.id}" + Rails.logger.info "[BreakEscape] Player preference: #{@player_preference.inspect}" + Rails.logger.info "[BreakEscape] Selected sprite: #{@player_preference.selected_sprite.inspect}" end # GET /games/:id/scenario @@ -1335,7 +1346,8 @@ module BreakEscape if current_player.respond_to?(:break_escape_preference) current_player.break_escape_preference elsif current_player.respond_to?(:preference) - current_player.preference + # Reload association to ensure fresh data + current_player.reload.preference end end diff --git a/app/controllers/break_escape/player_preferences_controller.rb b/app/controllers/break_escape/player_preferences_controller.rb index 6137988..93357fc 100644 --- a/app/controllers/break_escape/player_preferences_controller.rb +++ b/app/controllers/break_escape/player_preferences_controller.rb @@ -11,20 +11,52 @@ module BreakEscape # PATCH /break_escape/configuration def update + Rails.logger.info "[BreakEscape] Updating preference for player: #{current_player.class.name}##{current_player.id}" + Rails.logger.info "[BreakEscape] Params: #{player_preference_params.inspect}" + if @player_preference.update(player_preference_params) - flash[:notice] = 'Character configuration saved!' + Rails.logger.info "[BreakEscape] Preference updated successfully: selected_sprite=#{@player_preference.selected_sprite}, in_game_name=#{@player_preference.in_game_name}" + + respond_to do |format| + format.html do + flash[:notice] = 'Character configuration saved!' - # Redirect to game if came from validation flow - if params[:game_id].present? - redirect_to game_path(params[:game_id]) - else - redirect_to configuration_path + # Redirect to game if came from validation flow + if params[:game_id].present? + redirect_to game_path(params[:game_id]) + else + redirect_to configuration_path + end + end + format.json do + render json: { + success: true, + message: 'Character configuration saved!', + data: { + selected_sprite: @player_preference.selected_sprite, + in_game_name: @player_preference.in_game_name + } + } + end end else - flash.now[:alert] = 'Please select a character sprite.' - @available_sprites = PlayerPreference::AVAILABLE_SPRITES - @scenario = load_scenario_if_validating - render :show, status: :unprocessable_entity + Rails.logger.error "[BreakEscape] Failed to update preference: #{@player_preference.errors.full_messages.join(', ')}" + + respond_to do |format| + format.html do + flash.now[:alert] = 'Please select a character sprite.' + @available_sprites = PlayerPreference::AVAILABLE_SPRITES + @scenario = load_scenario_if_validating + render :show, status: :unprocessable_entity + end + format.json do + render json: { + success: false, + error: 'Please select a character sprite.', + errors: @player_preference.errors.full_messages + }, status: :unprocessable_entity + end + end end end @@ -32,13 +64,15 @@ module BreakEscape def set_player_preference @player_preference = current_player_preference || create_default_preference + Rails.logger.info "[BreakEscape] set_player_preference: #{@player_preference.inspect}" end def current_player_preference if current_player.respond_to?(:break_escape_preference) current_player.break_escape_preference elsif current_player.respond_to?(:preference) - current_player.preference + # Reload association to ensure fresh data + current_player.reload.preference end end diff --git a/app/views/break_escape/games/show.html.erb b/app/views/break_escape/games/show.html.erb index 35f5268..64604ac 100644 --- a/app/views/break_escape/games/show.html.erb +++ b/app/views/break_escape/games/show.html.erb @@ -34,6 +34,7 @@ + @@ -127,6 +128,9 @@ + <%# Player Preferences Modal %> + <%= render 'break_escape/player_preferences/modal' %> + <%# Popup Overlay %>
@@ -138,8 +142,11 @@ assetsPath: '/break_escape/assets', csrfToken: '<%= form_authenticity_token %>', hacktivityMode: <%= BreakEscape::Mission.hacktivity_mode? ? 'true' : 'false' %>, - vmSetId: <%= @game.player_state.is_a?(Hash) ? (@game.player_state['vm_set_id'] || 'null') : 'null' %> + vmSetId: <%= @game.player_state.is_a?(Hash) ? (@game.player_state['vm_set_id'] || 'null') : 'null' %>, + playerSprite: '<%= @player_preference&.selected_sprite || 'male_hacker' %>' }; + console.log('🔧 breakEscapeConfig initialized:', window.breakEscapeConfig); + console.log('🎨 Player sprite from server:', window.breakEscapeConfig.playerSprite); <%# Load required libraries before the game module %> diff --git a/app/views/break_escape/player_preferences/_modal.html.erb b/app/views/break_escape/player_preferences/_modal.html.erb new file mode 100644 index 0000000..eb013f7 --- /dev/null +++ b/app/views/break_escape/player_preferences/_modal.html.erb @@ -0,0 +1,214 @@ +<%# Player Preferences Modal - Rendered inline in game view %> + + + diff --git a/public/break_escape/css/player_preferences.css b/public/break_escape/css/player_preferences.css index 64efeaf..bdc7b30 100644 --- a/public/break_escape/css/player_preferences.css +++ b/public/break_escape/css/player_preferences.css @@ -340,3 +340,145 @@ h1 { font-size: 10px; } } + +/* ===== MODAL OVERLAY STYLING ===== */ + +#player-preferences-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + z-index: 4000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.player-preferences-modal-content { + background: #2a2a2a; + border: 2px solid #00ff00; + max-width: 1000px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + padding: 20px; + position: relative; + box-shadow: 0 0 40px rgba(0, 255, 0, 0.3); +} + +.player-preferences-modal-content .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 2px solid #00ff00; +} + +.player-preferences-modal-content .modal-header h2 { + margin: 0; + color: #00ff00; + font-size: 24px; + text-transform: uppercase; + font-family: 'Pixelify Sans', Arial, sans-serif; +} + +.modal-close-button { + background: #ff0000; + color: #fff; + border: 2px solid #fff; + border-radius: 0; + width: 32px; + height: 32px; + font-size: 24px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + font-family: 'Press Start 2P', monospace; +} + +.modal-close-button:hover { + background: #cc0000; +} + +/* Modal-specific form actions */ +.player-preferences-modal-content .modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 20px; + border-top: 2px solid #333; +} + +.player-preferences-modal-content .btn { + padding: 10px 20px; + font-size: 14px; + font-family: 'Pixelify Sans', Arial, sans-serif; + font-weight: bold; + border: 2px solid; + cursor: pointer; + text-transform: uppercase; + transition: all 0.2s; +} + +.player-preferences-modal-content .btn-primary { + background: #00ff00; + color: #000; + border-color: #00ff00; +} + +.player-preferences-modal-content .btn-primary:hover { + background: #00cc00; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); +} + +.player-preferences-modal-content .btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.player-preferences-modal-content .btn-secondary { + background: #666; + color: #fff; + border-color: #666; + text-decoration: none; + display: inline-block; +} + +.player-preferences-modal-content .btn-secondary:hover { + background: #888; +} + +/* Responsive adjustments for modal */ +@media (max-width: 768px) { + #player-preferences-modal { + padding: 10px; + } + + .player-preferences-modal-content { + padding: 15px; + } + + .player-preferences-modal-content .modal-header h2 { + font-size: 18px; + } +} + +@media (max-width: 480px) { + .player-preferences-modal-content .modal-header h2 { + font-size: 14px; + } + + .modal-close-button { + width: 28px; + height: 28px; + font-size: 20px; + } +} diff --git a/public/break_escape/js/core/game.js b/public/break_escape/js/core/game.js index 07186ba..1fdec4b 100644 --- a/public/break_escape/js/core/game.js +++ b/public/break_escape/js/core/game.js @@ -633,7 +633,7 @@ export async function create() { const playerData = { id: 'player', displayName: window.gameState?.playerName || window.gameScenario?.player?.displayName || 'Agent 0x00', - spriteSheet: window.gameScenario?.player?.spriteSheet || 'hacker', + spriteSheet: window.breakEscapeConfig?.playerSprite || window.gameScenario?.player?.spriteSheet || 'male_hacker', spriteTalk: window.gameScenario?.player?.spriteTalk || 'assets/characters/hacker-talk.png', metadata: {} }; diff --git a/public/break_escape/js/core/player.js b/public/break_escape/js/core/player.js index 239e4b4..872a827 100644 --- a/public/break_escape/js/core/player.js +++ b/public/break_escape/js/core/player.js @@ -54,6 +54,119 @@ export function resumeKeyboardInput() { console.log('🔓 Keyboard input RESUMED (keyboardPaused = false)'); } +/** + * Update the player sprite to use a different character + * This allows changing the player's appearance mid-game + * @param {string} newSpriteKey - The texture key for the new sprite + */ +export async function updatePlayerSprite(newSpriteKey) { + if (!player || !gameRef) { + console.error('❌ Cannot update player sprite - player or game not initialized'); + return false; + } + + console.log('🔄 Updating player sprite from', player.texture.key, 'to', newSpriteKey); + + // Check if the new sprite is already loaded + const newTexture = gameRef.textures.get(newSpriteKey); + if (!newTexture || newTexture.key === '__MISSING') { + console.log('📦 Loading new sprite:', newSpriteKey); + + // Load the new sprite + const assetsPath = window.breakEscapeConfig?.assetsPath || '/break_escape/assets'; + const atlasPath = `${assetsPath}/characters/${newSpriteKey}.png`; + const jsonPath = `${assetsPath}/characters/${newSpriteKey}.json`; + + try { + await new Promise((resolve, reject) => { + gameRef.load.atlas(newSpriteKey, atlasPath, jsonPath); + gameRef.load.once('complete', resolve); + gameRef.load.once('loaderror', reject); + gameRef.load.start(); + }); + console.log('✅ New sprite loaded:', newSpriteKey); + } catch (error) { + console.error('❌ Failed to load new sprite:', error); + return false; + } + } + + // Store current state + const currentDirection = player.direction || 'down'; + const wasMoving = player.isMoving; + + // Detect if new sprite is atlas or legacy + const frames = gameRef.textures.get(newSpriteKey).getFrameNames(); + const isAtlas = frames.length > 0 && typeof frames[0] === 'string' && + (frames[0].includes('breathing-idle') || frames[0].includes('walk_') || frames[0].includes('_frame_')); + + // Update collision box for new sprite type + if (isAtlas) { + player.body.setSize(18, 10); + player.body.setOffset(31, 66); + console.log('🎮 Updated collision box for atlas sprite (80x80)'); + } else { + player.body.setSize(15, 10); + player.body.setOffset(25, 50); + console.log('🎮 Updated collision box for legacy sprite (64x64)'); + } + + // Store the atlas flag on player + player.isAtlas = isAtlas; + + // Update scenario reference BEFORE recreating animations so createPlayerAnimations() uses the new sprite + if (window.gameScenario?.player) { + window.gameScenario.player.spriteSheet = newSpriteKey; + } + + // Destroy old animations before creating new ones (they reference the old sprite texture) + const animKeysToDestroy = [ + 'idle-down', 'idle-up', 'idle-left', 'idle-right', + 'idle-down-left', 'idle-down-right', 'idle-up-left', 'idle-up-right', + 'walk-down', 'walk-up', 'walk-left', 'walk-right', + 'walk-down-left', 'walk-down-right', 'walk-up-left', 'walk-up-right', + 'punch-down', 'punch-up', 'punch-left', 'punch-right' + ]; + + // Also destroy punch animations with compass directions + const punchDirections = ['north', 'south', 'east', 'west', 'north-east', 'north-west', 'south-east', 'south-west']; + punchDirections.forEach(dir => { + animKeysToDestroy.push(`cross-punch_${dir}`); + animKeysToDestroy.push(`lead-jab_${dir}`); + }); + + animKeysToDestroy.forEach(key => { + if (gameRef.anims.exists(key)) { + gameRef.anims.remove(key); + } + }); + + console.log('🗑️ Removed old animations'); + + // Change the texture of the existing sprite + let initialFrame; + if (isAtlas) { + const breathingIdleFrames = frames.filter(f => f.startsWith('breathing-idle_south_frame_')); + initialFrame = breathingIdleFrames.length > 0 ? breathingIdleFrames[0] : frames[0]; + } else { + initialFrame = 20; + } + + player.setTexture(newSpriteKey, initialFrame); + + // Recreate animations for the new sprite (now reads updated scenario) + createPlayerAnimations(); + + // Play appropriate animation + const animKey = wasMoving ? `walk-${currentDirection}` : `idle-${currentDirection}`; + if (player.anims.exists(animKey)) { + player.anims.play(animKey, true); + } + + console.log('✅ Player sprite updated successfully to', newSpriteKey); + return true; +} + // Create player sprite export function createPlayer(gameInstance) { gameRef = gameInstance; @@ -64,9 +177,15 @@ export function createPlayer(gameInstance) { const startRoomId = scenario ? scenario.startRoom : 'reception'; const startRoomPosition = getStartingRoomCenter(startRoomId); - // Get player sprite from scenario - const playerSprite = window.gameScenario?.player?.spriteSheet || 'hacker'; - console.log(`🎮 Loading player sprite: ${playerSprite} (from ${window.gameScenario?.player ? 'scenario' : 'default'})`); + // Get player sprite - prioritize saved preference over scenario default + const playerSprite = window.breakEscapeConfig?.playerSprite || window.gameScenario?.player?.spriteSheet || 'male_hacker'; + const source = window.breakEscapeConfig?.playerSprite ? 'saved preference' : (window.gameScenario?.player ? 'scenario' : 'default'); + console.log(`🎮 Loading player sprite: ${playerSprite} (from ${source})`); + + // Update scenario to match saved preference + if (window.gameScenario?.player && window.breakEscapeConfig?.playerSprite) { + window.gameScenario.player.spriteSheet = window.breakEscapeConfig.playerSprite; + } // Check if this is an atlas sprite (has named frames) or legacy (numbered frames) const texture = gameInstance.textures.get(playerSprite); @@ -1040,9 +1159,11 @@ function getStartingRoomCenter(startRoomId) { window.createPlayer = createPlayer; window.pauseKeyboardInput = pauseKeyboardInput; window.resumeKeyboardInput = resumeKeyboardInput; +window.updatePlayerSprite = updatePlayerSprite; console.log('✅ Player module loaded - keyboard control functions exported to window:', { createPlayer: typeof window.createPlayer, pauseKeyboardInput: typeof window.pauseKeyboardInput, - resumeKeyboardInput: typeof window.resumeKeyboardInput + resumeKeyboardInput: typeof window.resumeKeyboardInput, + updatePlayerSprite: typeof window.updatePlayerSprite }); \ No newline at end of file diff --git a/public/break_escape/js/ui/hud.js b/public/break_escape/js/ui/hud.js index 467a4ac..9ec55db 100644 --- a/public/break_escape/js/ui/hud.js +++ b/public/break_escape/js/ui/hud.js @@ -163,18 +163,66 @@ export class PlayerHUD { * Open player preferences modal */ openPlayerPreferences() { - console.log('🎮 Opening player preferences'); + console.log('🎮 Opening player preferences modal'); // Check if player preferences modal exists in the DOM const preferencesModal = document.getElementById('player-preferences-modal'); if (preferencesModal) { - preferencesModal.style.display = 'block'; + // Initialize the sprite preview when opening + if (typeof window.initPlayerPreferencesModal === 'function') { + window.initPlayerPreferencesModal(); + } + + // Show the modal + preferencesModal.style.display = 'flex'; + + // Pause the game while modal is open + if (this.scene && this.scene.scene.isPaused() === false) { + this.scene.scene.pause(); + } } else { - // Fallback: show alert for now - alert('Player preferences modal not yet implemented. This will open sprite selection.'); + console.error('❌ Player preferences modal not found in DOM'); + alert('Player preferences modal is not available. Please refresh the page.'); } } + /** + * Close player preferences modal + */ + closePlayerPreferences() { + console.log('🎮 Closing player preferences modal'); + + const preferencesModal = document.getElementById('player-preferences-modal'); + if (preferencesModal) { + preferencesModal.style.display = 'none'; + + // Resume the game + if (this.scene && this.scene.scene.isPaused() === true) { + this.scene.scene.resume(); + } + } + } + + /** + * Update avatar sprite to a new sprite + * @param {string} newSpriteKey - The key of the new sprite to display + */ + updateAvatarSprite(newSpriteKey) { + console.log('👤 Updating avatar sprite to:', newSpriteKey); + + if (!this.avatarImg) { + console.error('❌ Avatar image element not found'); + return; + } + + // Update the avatar image + const headshotPath = this.getHeadshotPath(newSpriteKey); + this.avatarImg.src = headshotPath; + this.avatarImg.alt = newSpriteKey; + + console.log('✅ Avatar updated to:', newSpriteKey); + } + /** * Set up mode toggle button */ diff --git a/public/break_escape/js/ui/sprite-grid.js b/public/break_escape/js/ui/sprite-grid.js index 4e005a9..5bf2df2 100644 --- a/public/break_escape/js/ui/sprite-grid.js +++ b/public/break_escape/js/ui/sprite-grid.js @@ -17,9 +17,15 @@ let phaserGame = null; let currentPreviewSprite = null; let initialSelectedSprite = null; -export function initializeSpritePreview(sprites, selectedSprite) { - console.log('🎨 Initializing sprite preview...', { sprites: sprites.length, selectedSprite }); +export function initializeSpritePreview(sprites, selectedSprite, containerIdOverride = null) { + console.log('🎨 Initializing sprite preview...', { sprites: sprites.length, selectedSprite, containerIdOverride }); initialSelectedSprite = selectedSprite || null; + + // Use custom container ID if provided, otherwise use default + const containerId = containerIdOverride || 'sprite-preview-canvas-container'; + const formId = containerIdOverride === 'sprite-preview-canvas-container-modal' + ? 'preference-form-modal' + : 'preference-form'; class PreviewScene extends Phaser.Scene { constructor() { @@ -31,7 +37,7 @@ export function initializeSpritePreview(sprites, selectedSprite) { loadAndShowSprite(this, initialSelectedSprite); } // Listen for radio changes - const form = document.getElementById('preference-form'); + const form = document.getElementById(formId); if (form) { const radios = form.querySelectorAll('input[type="radio"][name*="selected_sprite"]'); radios.forEach(radio => { @@ -46,7 +52,7 @@ export function initializeSpritePreview(sprites, selectedSprite) { const config = { type: Phaser.AUTO, - parent: 'sprite-preview-canvas-container', + parent: containerId, width: PREVIEW_SIZE, height: PREVIEW_SIZE, transparent: true,