mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat: Add player preferences modal for character configuration and enhance sprite selection functionality
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<link rel="stylesheet" href="/break_escape/css/notifications.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/panels.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/hud.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/player_preferences.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/minigames-framework.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/dusting.css">
|
||||
<link rel="stylesheet" href="/break_escape/css/lockpicking.css">
|
||||
@@ -127,6 +128,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Player Preferences Modal %>
|
||||
<%= render 'break_escape/player_preferences/modal' %>
|
||||
|
||||
<%# Popup Overlay %>
|
||||
<div class="popup-overlay"></div>
|
||||
|
||||
@@ -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);
|
||||
</script>
|
||||
|
||||
<%# Load required libraries before the game module %>
|
||||
|
||||
214
app/views/break_escape/player_preferences/_modal.html.erb
Normal file
214
app/views/break_escape/player_preferences/_modal.html.erb
Normal file
@@ -0,0 +1,214 @@
|
||||
<%# Player Preferences Modal - Rendered inline in game view %>
|
||||
<div id="player-preferences-modal" class="modal-overlay" style="display: none;" onclick="if(event.target === this) window.playerHUD.closePlayerPreferences()">
|
||||
<div class="player-preferences-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Character Configuration</h2>
|
||||
<button class="modal-close-button" onclick="window.playerHUD.closePlayerPreferences()">×</button>
|
||||
</div>
|
||||
|
||||
<%= form_with model: @player_preference,
|
||||
url: configuration_path,
|
||||
method: :patch,
|
||||
local: false,
|
||||
id: 'preference-form-modal',
|
||||
data: { turbo: false } do |f| %>
|
||||
|
||||
<!-- In-Game Name -->
|
||||
<div class="form-group">
|
||||
<%= f.label :in_game_name, "Your Code Name" %>
|
||||
<%= f.text_field :in_game_name,
|
||||
class: 'form-control',
|
||||
maxlength: 20,
|
||||
placeholder: 'Zero' %>
|
||||
<small>1-20 characters (letters, numbers, spaces, underscores only)</small>
|
||||
</div>
|
||||
|
||||
<!-- Sprite Selection -->
|
||||
<div class="form-group">
|
||||
<%= f.label :selected_sprite, "Select Your Character" %>
|
||||
<% if @player_preference.selected_sprite.blank? %>
|
||||
<p class="selection-required">⚠️ Character selection required</p>
|
||||
<% end %>
|
||||
|
||||
<div class="sprite-selection-layout">
|
||||
<!-- 160x160 animated preview of selected sprite -->
|
||||
<div class="sprite-preview-large">
|
||||
<div id="sprite-preview-canvas-container-modal"></div>
|
||||
<p class="preview-label">Selected character</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid of static headshots -->
|
||||
<div class="sprite-grid" id="sprite-selection-grid-modal">
|
||||
<% @available_sprites.each_with_index do |sprite, index| %>
|
||||
<%
|
||||
# In-game modal: no scenario restrictions (or use current game's scenario if available)
|
||||
is_valid = true
|
||||
is_selected = @player_preference.selected_sprite == sprite
|
||||
%>
|
||||
|
||||
<label for="sprite_modal_<%= sprite %>"
|
||||
class="sprite-card <%= 'invalid' unless is_valid %> <%= 'selected' if is_selected %>"
|
||||
data-sprite="<%= sprite %>">
|
||||
|
||||
<div class="sprite-headshot-container">
|
||||
<%= image_tag sprite_headshot_path(sprite),
|
||||
class: 'sprite-headshot',
|
||||
alt: sprite.humanize,
|
||||
loading: 'lazy',
|
||||
onerror: "this.onerror=null; this.style.display='none'; var n=this.nextElementSibling; if(n) n.classList.remove('headshot-fallback-hidden');" %>
|
||||
<span class="headshot-fallback headshot-fallback-hidden"><%= sprite.humanize %></span>
|
||||
</div>
|
||||
|
||||
<div class="sprite-info">
|
||||
<%= f.radio_button :selected_sprite,
|
||||
sprite,
|
||||
id: "sprite_modal_#{sprite}",
|
||||
disabled: !is_valid,
|
||||
class: 'sprite-radio' %>
|
||||
<span class="sprite-label"><%= sprite.humanize %></span>
|
||||
</div>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="form-actions modal-footer">
|
||||
<%= f.submit 'Save Configuration', class: 'btn btn-primary', id: 'save-preferences-btn' %>
|
||||
<button type="button" class="btn btn-secondary" onclick="window.playerHUD.closePlayerPreferences()">Cancel</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" nonce="<%= content_security_policy_nonce %>">
|
||||
import { initializeSpritePreview } from '/break_escape/js/ui/sprite-grid.js?v=2';
|
||||
|
||||
// Initialize sprite preview when modal is shown
|
||||
window.initPlayerPreferencesModal = function() {
|
||||
const sprites = <%= raw @available_sprites.to_json %>;
|
||||
// Use current sprite or default
|
||||
let selectedSprite = '<%= @player_preference.selected_sprite.presence || "female_hacker_hood" %>';
|
||||
|
||||
initializeSpritePreview(sprites, selectedSprite, 'sprite-preview-canvas-container-modal');
|
||||
|
||||
// Click on headshot selects radio and updates selected class
|
||||
const grid = document.getElementById('sprite-selection-grid-modal');
|
||||
if (grid) {
|
||||
grid.addEventListener('click', function(e) {
|
||||
const label = e.target.closest('label.sprite-card');
|
||||
if (!label) return;
|
||||
const radio = label.querySelector('input[type="radio"]');
|
||||
if (radio && !radio.disabled) {
|
||||
radio.checked = true;
|
||||
radio.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
// Remove selected from all, add to clicked
|
||||
document.querySelectorAll('#sprite-selection-grid-modal label.sprite-card').forEach(l => l.classList.remove('selected'));
|
||||
label.classList.add('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('player-preferences-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
if (window.playerHUD) {
|
||||
window.playerHUD.closePlayerPreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submission via AJAX
|
||||
const form = document.getElementById('preference-form-modal');
|
||||
if (form) {
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const submitButton = document.getElementById('save-preferences-btn');
|
||||
|
||||
// Disable submit button during request
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'PATCH',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Success - get the saved values from server response
|
||||
const result = await response.json();
|
||||
const newSprite = result.data?.selected_sprite;
|
||||
const newName = result.data?.in_game_name;
|
||||
|
||||
console.log('✅ Configuration saved to server:', { sprite: newSprite, name: newName });
|
||||
|
||||
// Update breakEscapeConfig
|
||||
if (window.breakEscapeConfig) {
|
||||
window.breakEscapeConfig.playerSprite = newSprite;
|
||||
}
|
||||
|
||||
// Update HUD avatar
|
||||
if (window.playerHUD) {
|
||||
window.playerHUD.updateAvatarSprite(newSprite);
|
||||
}
|
||||
|
||||
// Update player sprite in game using the player module's update function
|
||||
if (window.updatePlayerSprite) {
|
||||
try {
|
||||
await window.updatePlayerSprite(newSprite);
|
||||
|
||||
// Close modal after successful update
|
||||
if (window.playerHUD) {
|
||||
window.playerHUD.closePlayerPreferences();
|
||||
}
|
||||
|
||||
// Show success notification including name if it was changed
|
||||
if (window.NotificationManager) {
|
||||
const message = newName
|
||||
? `Configuration saved! Code name: ${newName}`
|
||||
: 'Configuration saved successfully!';
|
||||
window.NotificationManager.show(message, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update player sprite:', error);
|
||||
alert('Sprite changed but display update failed. Please reload the page.');
|
||||
}
|
||||
} else {
|
||||
console.warn('updatePlayerSprite not available, closing modal');
|
||||
if (window.playerHUD) {
|
||||
window.playerHUD.closePlayerPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable submit button
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Save Configuration';
|
||||
} else {
|
||||
// Error - show message
|
||||
const data = await response.json();
|
||||
alert(data.error || 'Failed to save configuration. Please try again.');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Save Configuration';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving preferences:', error);
|
||||
alert('Failed to save configuration. Please try again.');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Save Configuration';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user