SandGame/ui.js

274 lines
11 KiB
JavaScript
Raw Normal View History

2026-10-22 14:05:51 +00:00
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);
}
}
}