Files
GSM/gsm-frontend/src/components/WorldGenForm.jsx
Alexander Zielonka ff6adb093b Add Factorio World Management feature to GSM
- Add gsm-frontend to repository (React + Vite + TailwindCSS)
- New "Worlds" tab for Factorio server with:
  - List saved worlds with Start/Delete actions
  - Create new world with full map generation parameters
  - Preset selection (Default, Rich Resources, Rail World, etc.)
  - Save custom configurations as templates
- Show which save will be loaded in Overview tab
- Lock world management while server is running
- Backend changes deployed to server separately

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 15:42:14 +01:00

371 lines
12 KiB
JavaScript

import { useState } from 'react'
const SLIDER_VALUES = [0, 0.167, 0.5, 1, 2, 3]
const SLIDER_LABELS = ['None', 'Very Low', 'Low', 'Normal', 'High', 'Very High']
function valueToSlider(value) {
if (value === 0 || value === undefined) return 0
if (value <= 0.167) return 1
if (value <= 0.5) return 2
if (value <= 1) return 3
if (value <= 2) return 4
return 5
}
function sliderToValue(index) {
return SLIDER_VALUES[index]
}
function ValueSelect({ value, onChange }) {
const index = valueToSlider(value)
return (
<select
value={index}
onChange={(e) => onChange(sliderToValue(parseInt(e.target.value)))}
className="select text-xs py-1 px-2"
>
{SLIDER_LABELS.map((label, i) => (
<option key={i} value={i}>{label}</option>
))}
</select>
)
}
function ResourceRow({ label, settings, onChange }) {
const freq = settings?.frequency ?? 1
const size = settings?.size ?? 1
const richness = settings?.richness ?? 1
return (
<div className="grid grid-cols-4 gap-2 items-center py-2 border-b border-neutral-800 last:border-0">
<div className="text-neutral-300 text-sm">{label}</div>
<ValueSelect value={freq} onChange={(v) => onChange({ ...settings, frequency: v })} />
<ValueSelect value={size} onChange={(v) => onChange({ ...settings, size: v })} />
<ValueSelect value={richness} onChange={(v) => onChange({ ...settings, richness: v })} />
</div>
)
}
function SimpleSlider({ label, value, onChange }) {
const sliderIndex = valueToSlider(value)
return (
<div className="flex items-center gap-4 py-2 border-b border-neutral-800 last:border-0">
<div className="w-28 text-neutral-300 text-sm">{label}</div>
<input
type="range"
min="0"
max="5"
value={sliderIndex}
onChange={(e) => onChange(sliderToValue(parseInt(e.target.value)))}
className="slider flex-1 max-w-48"
/>
<span className="text-neutral-500 text-xs w-16">{SLIDER_LABELS[sliderIndex]}</span>
</div>
)
}
function EvolutionSlider({ label, value, onChange }) {
const displayValue = (value * 100).toFixed(0)
return (
<div className="flex items-center gap-4 py-2 border-b border-neutral-800 last:border-0">
<div className="w-28 text-neutral-300 text-sm">{label}</div>
<input
type="range"
min="0"
max="200"
value={value * 100}
onChange={(e) => onChange(parseInt(e.target.value) / 100)}
className="slider flex-1 max-w-48"
/>
<span className="text-neutral-500 text-xs w-16">{displayValue}%</span>
</div>
)
}
export default function WorldGenForm({
settings,
onSettingsChange,
presets,
templates,
onLoadPreset,
onLoadTemplate,
onSaveTemplate
}) {
const [selectedPreset, setSelectedPreset] = useState('default')
const [templateName, setTemplateName] = useState('')
const [showSaveTemplate, setShowSaveTemplate] = useState(false)
const handlePresetChange = (presetName) => {
setSelectedPreset(presetName)
onLoadPreset(presetName)
}
const handleSaveTemplate = () => {
if (templateName.trim()) {
onSaveTemplate(templateName.trim())
setTemplateName('')
setShowSaveTemplate(false)
}
}
const updateAutoplace = (key, value) => {
onSettingsChange({
...settings,
autoplace_controls: {
...settings.autoplace_controls,
[key]: value
}
})
}
const updateCliff = (key, value) => {
onSettingsChange({
...settings,
cliff_settings: {
...settings.cliff_settings,
[key]: value
}
})
}
const updateProperty = (key, value) => {
onSettingsChange({
...settings,
property_expression_names: {
...settings.property_expression_names,
[key]: value
}
})
}
return (
<div className="space-y-4 max-h-[50vh] overflow-y-auto pr-2">
{/* Preset & Template Selection */}
<div className="card p-3">
<div className="flex flex-wrap gap-3 items-center">
<div className="flex items-center gap-2">
<span className="text-neutral-400 text-sm">Preset:</span>
<select
value={selectedPreset}
onChange={(e) => handlePresetChange(e.target.value)}
className="select"
>
{presets?.map(p => (
<option key={p} value={p}>{p.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}</option>
))}
</select>
</div>
{templates?.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-neutral-400 text-sm">Template:</span>
<select
onChange={(e) => e.target.value && onLoadTemplate(parseInt(e.target.value))}
className="select"
defaultValue=""
>
<option value="">Select...</option>
{templates.map(t => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
</div>
)}
<button
onClick={() => setShowSaveTemplate(!showSaveTemplate)}
className="btn btn-outline text-sm ml-auto"
>
Save as Template
</button>
</div>
{showSaveTemplate && (
<div className="flex gap-2 mt-3 pt-3 border-t border-neutral-700">
<input
type="text"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="Template name..."
className="input flex-1"
/>
<button
onClick={handleSaveTemplate}
disabled={!templateName.trim()}
className="btn btn-primary"
>
Save
</button>
<button
onClick={() => setShowSaveTemplate(false)}
className="btn btn-secondary"
>
Cancel
</button>
</div>
)}
</div>
{/* Terrain Section */}
<div className="card p-4">
<h4 className="text-white font-medium text-sm mb-3">Terrain</h4>
<SimpleSlider
label="Water"
value={settings?.autoplace_controls?.water?.frequency ?? 1}
onChange={(v) => updateAutoplace('water', { frequency: v, size: v })}
/>
<SimpleSlider
label="Trees"
value={settings?.autoplace_controls?.trees?.frequency ?? 1}
onChange={(v) => updateAutoplace('trees', { frequency: v, size: v, richness: v })}
/>
<SimpleSlider
label="Cliffs"
value={settings?.cliff_settings?.richness ?? 1}
onChange={(v) => updateCliff('richness', v)}
/>
<SimpleSlider
label="Starting Area"
value={settings?.starting_area ?? 1}
onChange={(v) => onSettingsChange({ ...settings, starting_area: v })}
/>
</div>
{/* Resources Section */}
<div className="card p-4">
<h4 className="text-white font-medium text-sm mb-3">Resources</h4>
<div className="grid grid-cols-4 gap-2 text-neutral-500 text-xs mb-2 pb-2 border-b border-neutral-700">
<div></div>
<div className="text-center">Frequency</div>
<div className="text-center">Size</div>
<div className="text-center">Richness</div>
</div>
<ResourceRow
label="Iron Ore"
settings={settings?.autoplace_controls?.['iron-ore']}
onChange={(v) => updateAutoplace('iron-ore', v)}
/>
<ResourceRow
label="Copper Ore"
settings={settings?.autoplace_controls?.['copper-ore']}
onChange={(v) => updateAutoplace('copper-ore', v)}
/>
<ResourceRow
label="Coal"
settings={settings?.autoplace_controls?.coal}
onChange={(v) => updateAutoplace('coal', v)}
/>
<ResourceRow
label="Stone"
settings={settings?.autoplace_controls?.stone}
onChange={(v) => updateAutoplace('stone', v)}
/>
<ResourceRow
label="Uranium"
settings={settings?.autoplace_controls?.['uranium-ore']}
onChange={(v) => updateAutoplace('uranium-ore', v)}
/>
<ResourceRow
label="Crude Oil"
settings={settings?.autoplace_controls?.['crude-oil']}
onChange={(v) => updateAutoplace('crude-oil', v)}
/>
</div>
{/* Enemies Section */}
<div className="card p-4">
<h4 className="text-white font-medium text-sm mb-3">Enemies</h4>
<div className="grid grid-cols-4 gap-2 text-neutral-500 text-xs mb-2 pb-2 border-b border-neutral-700">
<div></div>
<div className="text-center">Frequency</div>
<div className="text-center">Size</div>
<div className="text-center">Richness</div>
</div>
<ResourceRow
label="Biter Bases"
settings={settings?.autoplace_controls?.['enemy-base']}
onChange={(v) => updateAutoplace('enemy-base', v)}
/>
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-neutral-700">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings?.peaceful_mode ?? false}
onChange={(e) => onSettingsChange({ ...settings, peaceful_mode: e.target.checked })}
className="w-4 h-4 rounded border-neutral-600 bg-neutral-800 text-white focus:ring-neutral-500"
/>
<span className="text-neutral-300 text-sm">Peaceful Mode</span>
</label>
</div>
</div>
{/* Evolution Section */}
<div className="card p-4">
<h4 className="text-white font-medium text-sm mb-3">Evolution</h4>
<EvolutionSlider
label="Time Factor"
value={settings?.property_expression_names?.['enemy-evolution-factor-by-time'] ?? 1}
onChange={(v) => updateProperty('enemy-evolution-factor-by-time', v)}
/>
<EvolutionSlider
label="Pollution Factor"
value={settings?.property_expression_names?.['enemy-evolution-factor-by-pollution'] ?? 1}
onChange={(v) => updateProperty('enemy-evolution-factor-by-pollution', v)}
/>
<EvolutionSlider
label="Destroy Factor"
value={settings?.property_expression_names?.['enemy-evolution-factor-by-killing-spawners'] ?? 1}
onChange={(v) => updateProperty('enemy-evolution-factor-by-killing-spawners', v)}
/>
</div>
{/* Advanced Section */}
<div className="card p-4">
<h4 className="text-white font-medium text-sm mb-3">Advanced</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="form-label">Seed (empty = random)</label>
<input
type="text"
value={settings?.seed ?? ''}
onChange={(e) => onSettingsChange({ ...settings, seed: e.target.value ? parseInt(e.target.value) : null })}
placeholder="Random"
className="input"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="form-label">Width (0=infinite)</label>
<input
type="number"
value={settings?.width ?? 0}
onChange={(e) => onSettingsChange({ ...settings, width: parseInt(e.target.value) || 0 })}
className="input"
/>
</div>
<div>
<label className="form-label">Height (0=infinite)</label>
<input
type="number"
value={settings?.height ?? 0}
onChange={(e) => onSettingsChange({ ...settings, height: parseInt(e.target.value) || 0 })}
className="input"
/>
</div>
</div>
</div>
</div>
</div>
)
}