// Debug alert for mobile debugging
if (typeof debugAlert === 'function') {
debugAlert('movement.js loaded');
}
// Movement settings for each group
let movementSettings = {};
let currentMovementGroup = 0;
// Movement configuration options
const MOVEMENT_SETTINGS = {
physics: {
gravity: { label: 'Gravity', type: 'number', default: 980, min: 0, max: 2000, step: 10, unit: 'px/s²' },
friction: { label: 'Friction', type: 'number', default: 0.8, min: 0, max: 1, step: 0.05, unit: '' },
bounce: { label: 'Bounce', type: 'number', default: 0.2, min: 0, max: 1, step: 0.05, unit: '' },
mass: { label: 'Mass', type: 'number', default: 1, min: 0.1, max: 10, step: 0.1, unit: 'kg' }
},
movement: {
walkSpeed: { label: 'Walk Speed', type: 'number', default: 150, min: 10, max: 500, step: 5, unit: 'px/s' },
runSpeed: { label: 'Run Speed', type: 'number', default: 250, min: 10, max: 800, step: 5, unit: 'px/s' },
jumpHeight: { label: 'Jump Height', type: 'number', default: 300, min: 0, max: 1000, step: 10, unit: 'px' },
acceleration: { label: 'Acceleration', type: 'number', default: 1000, min: 100, max: 3000, step: 50, unit: 'px/s²' }
},
collision: {
canCollide: { label: 'Can Collide', type: 'boolean', default: true },
canPush: { label: 'Can Push Others', type: 'boolean', default: false },
canBePushed: { label: 'Can Be Pushed', type: 'boolean', default: true },
solid: { label: 'Solid', type: 'boolean', default: true }
},
special: {
canClimb: { label: 'Can Climb', type: 'boolean', default: false },
canSwim: { label: 'Can Swim', type: 'boolean', default: false },
takeDamage: { label: 'Takes Damage', type: 'boolean', default: false },
dealDamage: { label: 'Deals Damage', type: 'boolean', default: false },
damageAmount: { label: 'Damage Amount', type: 'number', default: 10, min: 1, max: 100, step: 1, unit: 'hp' }
}
};
/**
* Open the movement overlay - main entry point called by index.html
*/
function openMovementOverlay() {
const overlayContent = document.getElementById('overlayContent');
overlayContent.innerHTML = `
<h2>Movement System š</h2>
<div id="movementTabs"></div>
<div id="movementSettings"></div>
`;
// Initialize movement settings for existing groups
initializeMovementSettings();
// Render the interface
renderMovementTabs();
renderMovementSettings();
}
/**
* Initialize movement settings for all existing groups
*/
function initializeMovementSettings() {
if (typeof groups !== 'undefined' && groups) {
groups.forEach((group, index) => {
if (!movementSettings[group.id]) {
movementSettings[group.id] = createDefaultMovementSettings(group.category);
}
});
}
}
/**
* Create default movement settings based on group category
* @param {string} category - The tile group category
* @returns {Object} Default movement settings
*/
function createDefaultMovementSettings(category) {
const settings = {};
// Copy default values from MOVEMENT_SETTINGS
for (const categoryKey in MOVEMENT_SETTINGS) {
settings[categoryKey] = {};
for (const settingKey in MOVEMENT_SETTINGS[categoryKey]) {
settings[categoryKey][settingKey] = MOVEMENT_SETTINGS[categoryKey][settingKey].default;
}
}
// Customize defaults based on tile category
switch (category) {
case 'Ground':
settings.collision.solid = true;
settings.collision.canCollide = true;
settings.physics.friction = 0.9;
break;
case 'Platform':
settings.collision.solid = false; // One-way collision
settings.collision.canCollide = true;
break;
case 'Pushable':
settings.collision.canBePushed = true;
settings.collision.canPush = false;
settings.physics.mass = 2;
break;
case 'Passable':
settings.collision.canCollide = false;
settings.collision.solid = false;
break;
case 'Hazard':
settings.collision.solid = false;
settings.special.dealDamage = true;
settings.special.damageAmount = 25;
break;
case 'Conveyor':
settings.collision.solid = true;
settings.movement.walkSpeed = 200; // Affects player when standing on it
break;
case 'Climbable':
settings.collision.solid = false;
settings.special.canClimb = true;
break;
case 'Player':
settings.collision.canPush = true;
settings.special.takeDamage = true;
settings.movement.walkSpeed = 150;
settings.movement.jumpHeight = 300;
break;
case 'NPC':
settings.collision.canCollide = true;
settings.movement.walkSpeed = 100;
settings.movement.jumpHeight = 200;
break;
}
return settings;
}
/**
* Get category color for visual coding (matches tilepicker.js)
* @param {string} category - The category value
* @returns {string} CSS color value
*/
function getCategoryColor(category) {
const colors = {
'None': '#666',
'Ground': '#8B4513',
'Platform': '#DEB887',
'Pushable': '#CD853F',
'Passable': '#90EE90',
'Hazard': '#FF4500',
'Conveyor': '#4169E1',
'Climbable': '#228B22',
'Sensor': '#9370DB',
'Door': '#B8860B',
'SwitchableToggle': '#FF69B4',
'SwitchableOnce': '#FF1493',
'AnimationGround': '#FF6347',
'AnimationPassable': '#20B2AA',
'Player': '#FFD700',
'NPC': '#87CEEB'
};
return colors[category] || '#666';
}
/**
* Render the group tabs
*/
function renderMovementTabs() {
const tabContainer = document.getElementById('movementTabs');
if (!tabContainer) return;
tabContainer.innerHTML = '';
tabContainer.style.cssText = 'margin-bottom: 15px; display: flex; gap: 5px; align-items: center; flex-wrap: wrap;';
if (typeof groups === 'undefined' || !groups || groups.length === 0) {
tabContainer.innerHTML = '<div style="color: #888; padding: 10px;">No tile groups found. Create groups in the Tile Picker first.</div>';
return;
}
groups.forEach((group, index) => {
const btn = document.createElement('button');
const groupName = group.name || `Group ${index + 1}`;
const categoryColor = getCategoryColor(group.category);
btn.textContent = groupName;
btn.style.cssText = `
background: ${index === currentMovementGroup ? '#6cf' : '#555'};
color: ${index === currentMovementGroup ? '#000' : '#fff'};
border: 3px solid ${categoryColor};
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
position: relative;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
btn.onclick = () => {
currentMovementGroup = index;
renderMovementTabs();
renderMovementSettings();
};
btn.title = `${groupName}\nCategory: ${group.category}\nTiles: ${group.tiles ? group.tiles.length : 0}`;
// Add category indicator
const categoryIndicator = document.createElement('div');
categoryIndicator.style.cssText = `
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 3px;
background: ${categoryColor};
border-radius: 2px;
`;
btn.appendChild(categoryIndicator);
tabContainer.appendChild(btn);
});
}
/**
* Render movement settings for the current group
*/
function renderMovementSettings() {
const settingsContainer = document.getElementById('movementSettings');
if (!settingsContainer) return;
settingsContainer.innerHTML = '';
if (typeof groups === 'undefined' || !groups || groups.length === 0) {
settingsContainer.innerHTML = '<div style="color: #888; padding: 20px; text-align: center;">No groups available</div>';
return;
}
const currentGroup = groups[currentMovementGroup];
if (!currentGroup) return;
const groupSettings = movementSettings[currentGroup.id] || createDefaultMovementSettings(currentGroup.category);
movementSettings[currentGroup.id] = groupSettings;
settingsContainer.style.cssText = 'padding: 15px; background: #2a2a2a; border-radius: 8px; max-height: 70vh; overflow-y: auto;';
// Group header with category selector
const header = document.createElement('div');
header.style.cssText = 'margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #555;';
const titleRow = document.createElement('div');
titleRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;';
const titleDiv = document.createElement('div');
titleDiv.innerHTML = `
<h3 style="color: #6cf; margin: 0 0 5px 0;">${currentGroup.name || 'Unnamed Group'}</h3>
<div style="color: #ccc; font-size: 12px;">
Tiles: ${currentGroup.tiles ? currentGroup.tiles.length : 0} |
ID: ${currentGroup.id}
</div>
`;
const categoryDiv = document.createElement('div');
categoryDiv.style.cssText = 'display: flex; align-items: center; gap: 10px;';
const categoryLabel = document.createElement('label');
categoryLabel.textContent = 'Block Type:';
categoryLabel.style.cssText = 'color: #ccc; font-weight: bold; font-size: 14px;';
const categorySelect = document.createElement('select');
categorySelect.style.cssText = `
background: #555;
color: white;
border: 1px solid #777;
border-radius: 4px;
padding: 5px 8px;
font-size: 12px;
min-width: 150px;
`;
const categories = [
'None', 'Ground', 'Platform', 'Pushable', 'Passable', 'Hazard', 'Conveyor',
'Climbable', 'Sensor', 'Door', 'SwitchableToggle', 'SwitchableOnce',
'AnimationGround', 'AnimationPassable', 'Player', 'NPC'
];
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat;
option.textContent = cat;
option.selected = cat === currentGroup.category;
categorySelect.appendChild(option);
});
categorySelect.onchange = () => {
currentGroup.category = categorySelect.value;
// Update movement settings based on new category
const newDefaults = createDefaultMovementSettings(categorySelect.value);
movementSettings[currentGroup.id] = newDefaults;
renderMovementSettings(); // Re-render to show updated settings
};
categoryDiv.appendChild(categoryLabel);
categoryDiv.appendChild(categorySelect);
titleRow.appendChild(titleDiv);
titleRow.appendChild(categoryDiv);
header.appendChild(titleRow);
// Add tile previews if tiles exist
if (currentGroup.tiles && currentGroup.tiles.length > 0) {
const tilesDiv = document.createElement('div');
tilesDiv.style.cssText = 'margin-top: 10px;';
const tilesLabel = document.createElement('div');
tilesLabel.textContent = 'Tiles in this group:';
tilesLabel.style.cssText = 'color: #ccc; font-size: 12px; margin-bottom: 8px;';
tilesDiv.appendChild(tilesLabel);
const tilesContainer = document.createElement('div');
tilesContainer.style.cssText = 'display: flex; gap: 5px; flex-wrap: wrap;';
currentGroup.tiles.forEach(tile => {
const tileWrapper = document.createElement('div');
tileWrapper.style.cssText = 'position: relative; display: inline-block;';
// Create canvas for tile display
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
canvas.style.cssText = `border: 2px solid ${getCategoryColor(currentGroup.category)}; border-radius: 4px; background: #000;`;
// Draw the tile
const ctx = canvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = tile.size;
tempCanvas.height = tile.size;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(tile.data, 0, 0);
// Scale to 32x32 for preview
ctx.drawImage(tempCanvas, 0, 0, tile.size, tile.size, 0, 0, 32, 32);
// Create ID badge
const idBadge = document.createElement('span');
idBadge.textContent = tile.uniqueId;
idBadge.style.cssText = 'position: absolute; bottom: -2px; right: -2px; background: rgba(0,0,0,0.8); color: #fff; font-size: 8px; padding: 1px 3px; border-radius: 2px; border: 1px solid #6cf;';
tileWrapper.appendChild(canvas);
tileWrapper.appendChild(idBadge);
tilesContainer.appendChild(tileWrapper);
});
tilesDiv.appendChild(tilesContainer);
header.appendChild(tilesDiv);
}
settingsContainer.appendChild(header);
// Render setting categories
for (const categoryKey in MOVEMENT_SETTINGS) {
const categoryDiv = document.createElement('div');
categoryDiv.style.cssText = 'margin-bottom: 20px;';
const categoryTitle = document.createElement('h4');
categoryTitle.textContent = categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1);
categoryTitle.style.cssText = 'color: #4a4; margin: 0 0 10px 0; font-size: 14px; text-transform: capitalize;';
categoryDiv.appendChild(categoryTitle);
const categorySettings = document.createElement('div');
categorySettings.style.cssText = 'background: #333; padding: 15px; border-radius: 6px; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;';
for (const settingKey in MOVEMENT_SETTINGS[categoryKey]) {
const setting = MOVEMENT_SETTINGS[categoryKey][settingKey];
const settingDiv = createSettingControl(categoryKey, settingKey, setting, groupSettings[categoryKey][settingKey]);
categorySettings.appendChild(settingDiv);
}
categoryDiv.appendChild(categorySettings);
settingsContainer.appendChild(categoryDiv);
}
// Export button
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export Movement Data';
exportBtn.style.cssText = `
background: #4a4;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 15px;
width: 100%;
`;
exportBtn.onclick = exportMovementData;
settingsContainer.appendChild(exportBtn);
}
/**
* Create a setting control element
* @param {string} categoryKey - The category key
* @param {string} settingKey - The setting key
* @param {Object} setting - The setting definition
* @param {*} currentValue - The current value
* @returns {HTMLElement} The setting control element
*/
function createSettingControl(categoryKey, settingKey, setting, currentValue) {
const settingDiv = document.createElement('div');
settingDiv.style.cssText = 'display: flex; flex-direction: column; gap: 5px;';
const label = document.createElement('label');
label.textContent = setting.label + (setting.unit ? ` (${setting.unit})` : '');
label.style.cssText = 'color: #ccc; font-size: 12px; font-weight: bold;';
settingDiv.appendChild(label);
let input;
if (setting.type === 'boolean') {
input = document.createElement('input');
input.type = 'checkbox';
input.checked = currentValue;
input.style.cssText = 'transform: scale(1.2);';
} else if (setting.type === 'number') {
input = document.createElement('input');
input.type = 'number';
input.value = currentValue;
input.min = setting.min || 0;
input.max = setting.max || 1000;
input.step = setting.step || 1;
input.style.cssText = 'background: #222; color: white; border: 1px solid #555; padding: 5px; border-radius: 3px; font-size: 12px;';
}
input.onchange = () => {
const newValue = setting.type === 'boolean' ? input.checked : parseFloat(input.value);
updateMovementSetting(categoryKey, settingKey, newValue);
};
settingDiv.appendChild(input);
return settingDiv;
}
/**
* Update a movement setting
* @param {string} categoryKey - The category key
* @param {string} settingKey - The setting key
* @param {*} value - The new value
*/
function updateMovementSetting(categoryKey, settingKey, value) {
const currentGroup = groups[currentMovementGroup];
if (!currentGroup) return;
if (!movementSettings[currentGroup.id]) {
movementSettings[currentGroup.id] = createDefaultMovementSettings(currentGroup.category);
}
movementSettings[currentGroup.id][categoryKey][settingKey] = value;
if (typeof debugAlert === 'function') {
debugAlert(`Updated ${currentGroup.name}.${categoryKey}.${settingKey} = ${value}`);
}
}
/**
* Export movement data as JSON
*/
function exportMovementData() {
try {
// Create complete movement configuration
const movementData = {
metadata: {
exportDate: new Date().toISOString(),
editor: "Tile Game Editor - Movement System",
version: "1.0.0"
},
globalSettings: {
defaultPhysics: MOVEMENT_SETTINGS.physics,
defaultMovement: MOVEMENT_SETTINGS.movement,
defaultCollision: MOVEMENT_SETTINGS.collision,
defaultSpecial: MOVEMENT_SETTINGS.special
},
groupMovementSettings: {}
};
// Add settings for each group
if (typeof groups !== 'undefined' && groups) {
groups.forEach(group => {
if (movementSettings[group.id]) {
movementData.groupMovementSettings[group.id] = {
groupName: group.name,
category: group.category,
tileCount: group.tiles ? group.tiles.length : 0,
settings: movementSettings[group.id]
};
}
});
}
const jsonString = JSON.stringify(movementData, null, 2);
// Try to copy to clipboard
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(jsonString).then(() => {
if (typeof debugAlert === 'function') {
debugAlert('Movement data copied! ' + Math.round(jsonString.length / 1024) + 'KB');
}
}).catch(() => {
fallbackCopy(jsonString);
});
} else {
fallbackCopy(jsonString);
}
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
if (successful && typeof debugAlert === 'function') {
debugAlert('Movement data copied (fallback)! ' + Math.round(text.length / 1024) + 'KB');
}
} catch (err) {
if (typeof debugAlert === 'function') {
debugAlert('Copy failed. Check console for data.');
}
console.log('Movement data:', text);
} finally {
document.body.removeChild(textarea);
}
}
} catch (error) {
if (typeof debugAlert === 'function') {
debugAlert('Export failed: ' + error.message);
}
}
}
if (typeof debugAlert === 'function') {
debugAlert('movement.js loaded successfully');
}