274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
|
|
import { config, saveSettings, defaultConfig, setConfig } from './config.js';
|
||
|
|
import { grid, cols, rows, selectedType, setSelectedType, setGrid, canvas, cellSize } from './state.js';
|
||
|
|
import { spawnCell } from './physics.js';
|
||
|
|
|
||
|
|
export const buttons = [
|
||
|
|
{ type: 'dirt', color: '#ffaa00', label: 'Dirt' },
|
||
|
|
{ type: 'plant', color: '#228B22', label: 'Plant' },
|
||
|
|
{ type: 'water', color: '#0000FF', label: 'Water' },
|
||
|
|
{ type: 'fire', color: '#FF5500', label: 'Fire' },
|
||
|
|
{ type: 'rock', color: '#808080', label: 'Rock' },
|
||
|
|
{ type: 'wood', color: '#8B4513', label: 'Wood' },
|
||
|
|
{ type: 'explosive', color: '#8B0000', label: 'TNT' },
|
||
|
|
{ type: 'acid', color: '#7FFF00', label: 'Acid' },
|
||
|
|
{ type: 'steam', color: '#E0FFFF', label: 'Steam' },
|
||
|
|
{ type: 'clear', color: '#FFFFFF', label: 'Clear' }
|
||
|
|
];
|
||
|
|
|
||
|
|
export function setupUI() {
|
||
|
|
// Settings Drawer
|
||
|
|
const drawer = document.getElementById('settingsContent');
|
||
|
|
const toggle = document.getElementById('toggleDrawer');
|
||
|
|
const drawerContainer = document.getElementById('settingsDrawer');
|
||
|
|
const pauseBtn = document.getElementById('togglePause');
|
||
|
|
|
||
|
|
toggle.addEventListener('click', () => {
|
||
|
|
drawerContainer.classList.toggle('open');
|
||
|
|
});
|
||
|
|
|
||
|
|
function updatePauseBtn() {
|
||
|
|
pauseBtn.textContent = config.time.paused ? '▶️' : '⏸️';
|
||
|
|
}
|
||
|
|
pauseBtn.addEventListener('click', () => {
|
||
|
|
config.time.paused = !config.time.paused;
|
||
|
|
saveSettings();
|
||
|
|
updatePauseBtn();
|
||
|
|
});
|
||
|
|
updatePauseBtn();
|
||
|
|
|
||
|
|
renderSettings(drawer);
|
||
|
|
|
||
|
|
// Elements Drawer
|
||
|
|
const elDrawer = document.getElementById('elementContent');
|
||
|
|
const elToggle = document.getElementById('toggleElements');
|
||
|
|
const elContainer = document.getElementById('elementDrawer');
|
||
|
|
|
||
|
|
elToggle.addEventListener('click', () => {
|
||
|
|
elContainer.classList.toggle('closed');
|
||
|
|
});
|
||
|
|
|
||
|
|
renderElements(elDrawer);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderElements(container) {
|
||
|
|
container.innerHTML = '';
|
||
|
|
buttons.forEach(btn => {
|
||
|
|
const button = document.createElement('button');
|
||
|
|
button.className = 'element-btn';
|
||
|
|
if (btn.type === selectedType) button.classList.add('selected');
|
||
|
|
button.style.backgroundColor = btn.color;
|
||
|
|
button.textContent = btn.label;
|
||
|
|
if (btn.type === 'clear' || btn.type === 'steam' || btn.type === 'acid') button.style.color = '#000';
|
||
|
|
if (btn.type === 'clear') {
|
||
|
|
button.style.marginTop = 'auto';
|
||
|
|
}
|
||
|
|
|
||
|
|
button.onclick = () => {
|
||
|
|
if (btn.type === 'clear') {
|
||
|
|
setGrid(new Array(cols).fill(null).map(() => new Array(rows).fill(null)));
|
||
|
|
} else {
|
||
|
|
setSelectedType(btn.type);
|
||
|
|
document.querySelectorAll('.element-btn').forEach(b => b.classList.remove('selected'));
|
||
|
|
button.classList.add('selected');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
container.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function renderSettings(container) {
|
||
|
|
container.innerHTML = ''; // Clear existing
|
||
|
|
|
||
|
|
function addControl(group, key, subkey, def) {
|
||
|
|
const container = document.createElement('div');
|
||
|
|
|
||
|
|
const labelEl = document.createElement('label');
|
||
|
|
let val = subkey ? config[group][key][subkey] : config[group][key];
|
||
|
|
|
||
|
|
labelEl.textContent = def.label;
|
||
|
|
|
||
|
|
labelEl.style.display = 'inline-block';
|
||
|
|
|
||
|
|
const input = document.createElement('input');
|
||
|
|
input.type = def.type || 'range';
|
||
|
|
|
||
|
|
if (def.type === 'checkbox') {
|
||
|
|
container.className = 'setting-checkbox';
|
||
|
|
input.checked = val;
|
||
|
|
input.style.marginRight = '10px';
|
||
|
|
input.addEventListener('change', (e) => {
|
||
|
|
if (subkey) config[group][key][subkey] = e.target.checked;
|
||
|
|
else config[group][key] = e.target.checked;
|
||
|
|
saveSettings();
|
||
|
|
});
|
||
|
|
container.append(input, labelEl);
|
||
|
|
} else if (def.type === 'radio') {
|
||
|
|
const fieldset = document.createElement('fieldset');
|
||
|
|
const legend = document.createElement('legend');
|
||
|
|
legend.textContent = def.label;
|
||
|
|
fieldset.appendChild(legend);
|
||
|
|
|
||
|
|
def.options.forEach(option => {
|
||
|
|
const radioId = `radio-${group}--`;
|
||
|
|
const radioContainer = document.createElement('div');
|
||
|
|
radioContainer.style.display = 'inline-block';
|
||
|
|
radioContainer.style.marginRight = '15px';
|
||
|
|
|
||
|
|
const radioInput = document.createElement('input');
|
||
|
|
radioInput.type = 'radio';
|
||
|
|
radioInput.name = `radio--`;
|
||
|
|
radioInput.value = option;
|
||
|
|
radioInput.id = radioId;
|
||
|
|
if (val === option) radioInput.checked = true;
|
||
|
|
|
||
|
|
radioInput.addEventListener('change', (e) => {
|
||
|
|
config[group][key] = e.target.value;
|
||
|
|
saveSettings();
|
||
|
|
});
|
||
|
|
|
||
|
|
const radioLabel = document.createElement('label');
|
||
|
|
radioLabel.textContent = option.charAt(0).toUpperCase() + option.slice(1);
|
||
|
|
radioLabel.setAttribute('for', radioId);
|
||
|
|
radioLabel.style.marginLeft = '5px';
|
||
|
|
|
||
|
|
radioContainer.append(radioInput, radioLabel);
|
||
|
|
fieldset.appendChild(radioContainer);
|
||
|
|
});
|
||
|
|
return fieldset;
|
||
|
|
} else {
|
||
|
|
container.className = 'setting-control';
|
||
|
|
input.min = def.min;
|
||
|
|
input.max = def.max;
|
||
|
|
input.step = def.step;
|
||
|
|
input.value = val;
|
||
|
|
const valueSpan = document.createElement('span');
|
||
|
|
valueSpan.textContent = val;
|
||
|
|
|
||
|
|
input.addEventListener('input', (e) => {
|
||
|
|
const newVal = parseFloat(e.target.value);
|
||
|
|
if (subkey) config[group][key][subkey] = newVal;
|
||
|
|
else config[group][key] = newVal;
|
||
|
|
saveSettings();
|
||
|
|
valueSpan.textContent = newVal.toFixed(Math.max(def.step.toString().split('.')[1]?.length || 0, 2));
|
||
|
|
});
|
||
|
|
container.append(labelEl, input, valueSpan);
|
||
|
|
}
|
||
|
|
|
||
|
|
return container;
|
||
|
|
}
|
||
|
|
|
||
|
|
const groups = {
|
||
|
|
time: [
|
||
|
|
{ key: 'speed', label: 'Speed', min: 0, max: 20, step: 1 }
|
||
|
|
],
|
||
|
|
global: [
|
||
|
|
{ key: 'gravity', label: 'Gravity', min: -1.0, max: 2.0, step: 0.1 },
|
||
|
|
{ key: 'drag', label: 'Drag', min: 0.1, max: 1.0, step: 0.05 }
|
||
|
|
],
|
||
|
|
explosive: [
|
||
|
|
{ key: 'force', label: 'Blast Force', min: 0, max: 20, step: 1 },
|
||
|
|
{ key: 'radius', label: 'Blast Reach', min: 1, max: 50, step: 1 },
|
||
|
|
{ key: 'destroy', subkey: 'rock', type: 'checkbox', label: 'Destroy Rock' },
|
||
|
|
{ key: 'destroy', subkey: 'wood', type: 'checkbox', label: 'Destroy Wood' },
|
||
|
|
{ key: 'destroy', subkey: 'dirt', type: 'checkbox', label: 'Destroy Dirt' },
|
||
|
|
{ key: 'destroy', subkey: 'plant', type: 'checkbox', label: 'Destroy Plant' },
|
||
|
|
{ key: 'destroy', subkey: 'water', type: 'checkbox', label: 'Destroy Water' },
|
||
|
|
{ key: 'destroy', subkey: 'acid', type: 'checkbox', label: 'Destroy Acid' }
|
||
|
|
],
|
||
|
|
acid: [
|
||
|
|
{ key: 'dissolveChance', label: 'Dissolve Rate', min: 0, max: 1.0, step: 0.01 },
|
||
|
|
{ key: 'sinkChance', label: 'Sink Rate', min: 0, max: 1.0, step: 0.05 },
|
||
|
|
{ key: 'explodeTNT', type: 'checkbox', label: 'Explode TNT' },
|
||
|
|
{ key: 'dissolve', subkey: 'rock', type: 'checkbox', label: 'Dissolve Rock' },
|
||
|
|
{ key: 'dissolve', subkey: 'wood', type: 'checkbox', label: 'Dissolve Wood' },
|
||
|
|
{ key: 'dissolve', subkey: 'dirt', type: 'checkbox', label: 'Dissolve Dirt' },
|
||
|
|
{ key: 'dissolve', subkey: 'plant', type: 'checkbox', label: 'Dissolve Plant' },
|
||
|
|
{ key: 'dissolve', subkey: 'water', type: 'checkbox', label: 'Infect Water' }
|
||
|
|
],
|
||
|
|
fire: [
|
||
|
|
{ key: 'burnChance', label: 'Fire Decay Rate', min: 0, max: 1.0, step: 0.01 }
|
||
|
|
],
|
||
|
|
water: [
|
||
|
|
{ key: 'flowChance', label: 'Flow Rate', min: 0, max: 1.0, step: 0.05 }
|
||
|
|
],
|
||
|
|
steam: [
|
||
|
|
{ key: 'condenseChance', label: 'Condense Rate', min: 0, max: 1.0, step: 0.01 }
|
||
|
|
],
|
||
|
|
plant: [
|
||
|
|
{ key: 'growChance', label: 'Growth Rate', min: 0, max: 1.0, step: 0.01 },
|
||
|
|
{ key: 'woodificationChance', label: 'Wood Rate', min: 0, max: 1.0, step: 0.01 },
|
||
|
|
{ key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.05 }
|
||
|
|
],
|
||
|
|
wood: [
|
||
|
|
{ key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.01 }
|
||
|
|
],
|
||
|
|
visual: [
|
||
|
|
{ key: 'style', type: 'radio', label: 'Style', options: ['blocky', 'bloby', 'fuzzy'] }
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const [groupName, settings] of Object.entries(groups)) {
|
||
|
|
const groupDiv = document.createElement('div');
|
||
|
|
groupDiv.className = 'setting-group';
|
||
|
|
const title = document.createElement('h3');
|
||
|
|
title.textContent = groupName.charAt(0).toUpperCase() + groupName.slice(1);
|
||
|
|
groupDiv.appendChild(title);
|
||
|
|
|
||
|
|
settings.forEach(s => {
|
||
|
|
groupDiv.appendChild(addControl(groupName, s.key, s.subkey, s));
|
||
|
|
});
|
||
|
|
container.appendChild(groupDiv);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Defaults Button
|
||
|
|
const resetBtn = document.createElement('button');
|
||
|
|
resetBtn.textContent = 'Reset to Defaults';
|
||
|
|
resetBtn.style.width = '100%';
|
||
|
|
resetBtn.style.padding = '10px';
|
||
|
|
resetBtn.style.marginTop = '20px';
|
||
|
|
resetBtn.style.cursor = 'pointer';
|
||
|
|
resetBtn.onclick = () => {
|
||
|
|
setConfig(JSON.parse(JSON.stringify(defaultConfig)));
|
||
|
|
saveSettings();
|
||
|
|
renderSettings(container);
|
||
|
|
};
|
||
|
|
container.appendChild(resetBtn);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function handleInput(e) {
|
||
|
|
const rect = canvas.getBoundingClientRect();
|
||
|
|
|
||
|
|
function getPos(clientX, clientY) {
|
||
|
|
return {
|
||
|
|
x: clientX - rect.left,
|
||
|
|
y: clientY - rect.top
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle Drawing
|
||
|
|
const drawAt = (clientX, clientY) => {
|
||
|
|
const {x, y} = getPos(clientX, clientY);
|
||
|
|
|
||
|
|
// No need to check isOverButton because DOM elements handle their own clicks
|
||
|
|
|
||
|
|
const col = Math.floor(x / cellSize);
|
||
|
|
const row = Math.floor(y / cellSize);
|
||
|
|
|
||
|
|
for (let i = -1; i <= 1; i++) {
|
||
|
|
for (let j = -1; j <= 1; j++) {
|
||
|
|
if (Math.random() > 0.5) {
|
||
|
|
spawnCell(col + i, row + j, selectedType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (e.type.startsWith('mouse')) {
|
||
|
|
drawAt(e.clientX, e.clientY);
|
||
|
|
} else if (e.type.startsWith('touch')) {
|
||
|
|
for (let i = 0; i < e.touches.length; i++) {
|
||
|
|
const t = e.touches[i];
|
||
|
|
drawAt(t.clientX, t.clientY);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|