feat: Add player preferences modal for character configuration and enhance sprite selection functionality

This commit is contained in:
Z. Cliffe Schreuders
2026-02-17 01:25:44 +00:00
parent 3d1570a030
commit 8dfc5f04f4
9 changed files with 610 additions and 26 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 %>

View 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>

View File

@@ -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;
}
}

View File

@@ -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: {}
};

View File

@@ -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
});

View File

@@ -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
*/

View File

@@ -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,