mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user