claw-code/.guardrails/examples/ruby/rails-ui/world-builder.rb

474 lines
15 KiB
Ruby

# frozen_string_literal: true
# Generative World Builder Component
# Rails 8 ViewComponent for world-building parameter editor
#
# Features:
# - Procedural generation parameter controls
# - Terrain, climate, and resource distribution editors
# - Real-time preview via Turbo Frames
# - Accessibility-first parameter adjustment
#
# @use Stimulus for interactive sliders and preview updates
# @test RSpec component tests
# @accessibility WCAG AA, keyboard navigation, ARIA live regions
module GameAdmin
class WorldBuilderComponent < AdminComponent
# World builder component for procedural generation
#
# Provides:
# - Terrain parameter controls (height, erosion, biome)
# - Climate system editor (temperature, precipitation)
# - Resource distribution (ores, vegetation, settlements)
# - Real-time preview rendering
#
# @param world_id [Integer] world to edit
# @param params [Hash] generation parameters
# @return [String] rendered HTML component
def render(world_id:, params: {})
@world_id = world_id
@params = params.permit(
:terrain_scale, :terrain_erosion, :biome_distribution,
:climate_temperature, :climate_precipitation,
:resource_density, :settlement_frequency
)
@world = World.find(world_id)
content_tag :div, class: 'world-builder', data: {
controller: 'world-builder',
world_builder_world_id: world_id,
world_builder_preview_url: '/admin/worlds/preview'
}, aria: {
label: 'World Building Editor',
role: 'application'
} do
concat render_header
concat render_parameter_panel
concat render_preview_panel
concat render_generation_controls
end
end
# Builder header with world info
def render_header
content_tag :header, class: 'builder-header' do
concat content_tag(:h1, 'World Builder', class: 'builder-title')
concat render_world_info(@world)
concat render_actions
end
end
# World information display
def render_world_info(world)
content_tag :div, class: 'world-info', aria: {
label: 'World information'
} do
concat content_tag(:h2, world.name, class: 'world-name')
concat content_tag(:p, "#{world.biomes.count} biomes", class: 'world-stat')
concat content_tag(:p, "#{world.size_km} km²", class: 'world-stat')
concat content_tag(:p, "Seed: #{world.seed}", class: 'world-seed')
end
end
# Action buttons (Save, Export, Regenerate)
def render_actions
content_tag :div, class: 'builder-actions' do
concat link_to('Save', "/admin/worlds/#{@world_id}", class: 'btn-primary')
concat link_to('Export', "/admin/worlds/#{@world_id}/export", class: 'btn-secondary')
concat link_to('Regenerate', "/admin/worlds/#{@world_id}/regenerate",
class: 'btn-warning',
data: {
confirm: 'Regenerate will erase current world. Continue?'
}
)
end
end
# Parameter panel with all generation controls
def render_parameter_panel
content_tag :section, class: 'parameter-panel', aria: {
label: 'Generation Parameters',
role: 'region'
}, data: {
world_builder_target: 'parameters'
} do
concat content_tag(:h2, 'Generation Parameters', class: 'panel-title')
# Terrain parameters
concat render_terrain_section
concat render_climate_section
concat render_biome_section
concat render_resource_section
end
end
# Terrain parameter controls
def render_terrain_section
content_tag :div, class: 'parameter-section terrain', data: {
section: 'terrain'
} do
concat content_tag(:h3, 'Terrain', class: 'section-title')
concat render_slider_control(
id: 'terrain_scale',
label: 'Scale',
min: 0, max: 100,
value: @params[:terrain_scale] || 50,
description: 'Feature size and frequency'
)
concat render_slider_control(
id: 'terrain_erosion',
label: 'Erosion',
min: 0, max: 100,
value: @params[:terrain_erosion] || 30,
description: 'Weathering and sedimentation'
)
concat render_slider_control(
id: 'terrain_ruggedness',
label: 'Ruggedness',
min: 0, max: 100,
value: @params[:terrain_ruggedness] || 40,
description: 'Height variation intensity'
)
end
end
# Climate parameter controls
def render_climate_section
content_tag :div, class: 'parameter-section climate', data: {
section: 'climate'
} do
concat content_tag(:h3, 'Climate', class: 'section-title')
concat render_slider_control(
id: 'climate_temperature',
label: 'Base Temperature',
min: -50, max: 50,
value: @params[:climate_temperature] || 20,
unit: '°C',
description: 'Global temperature baseline'
)
concat render_slider_control(
id: 'climate_precipitation',
label: 'Precipitation',
min: 0, max: 200,
value: @params[:climate_precipitation] || 100,
unit: 'mm/year',
description: 'Annual rainfall distribution'
)
concat render_slider_control(
id: 'climate_seasonality',
label: 'Seasonality',
min: 0, max: 100,
value: @params[:climate_seasonality] || 50,
description: 'Seasonal variation intensity'
)
end
end
# Biome distribution controls
def render_biome_section
content_tag :div, class: 'parameter-section biome', data: {
section: 'biome'
} do
concat content_tag(:h3, 'Biome Distribution', class: 'section-title')
concat render_biome_picker
concat render_vegetation_density_control
end
end
# Biome type picker with accessibility
def render_biome_picker
content_tag :div, class: 'biome-picker', role: 'group', aria: {
label: 'Biome types'
} do
BiomeType.all.each do |biome|
concat content_tag :div, class: 'biome-option' do
concat tag.label(class: 'biome-label') do
concat tag.checkbox_tag(
'biomes',
value: biome.id,
checked: @params[:biome_distribution]&.include?(biome.id.to_s)
)
concat content_tag(:span, biome.name, class: 'biome-name')
concat content_tag(:span, biome.icon, class: 'biome-icon', aria: {
hidden: true
})
end
end
end
end
end
# Vegetation density slider
def render_vegetation_density_control
render_slider_control(
id: 'vegetation_density',
label: 'Vegetation Density',
min: 0, max: 100,
value: @params[:vegetation_density] || 60,
description: 'Plant coverage percentage'
)
end
# Resource distribution controls
def render_resource_section
content_tag :div, class: 'parameter-section resource', data: {
section: 'resource'
} do
concat content_tag(:h3, 'Resource Distribution', class: 'section-title')
concat render_slider_control(
id: 'resource_density',
label: 'Resource Density',
min: 0, max: 100,
value: @params[:resource_density] || 40,
description: 'Ore and material spawn rate'
)
concat render_slider_control(
id: 'resource_rarity',
label: 'Rarity Gradient',
min: 0, max: 100,
value: @params[:resource_rarity] || 50,
description: 'Rare vs common distribution'
)
concat render_settlement_controls
end
end
# Settlement frequency and type controls
def render_settlement_controls
content_tag :div, class: 'settlement-controls' do
concat render_slider_control(
id: 'settlement_frequency',
label: 'Settlements',
min: 0, max: 100,
value: @params[:settlement_frequency] || 30,
description: 'Town/city spawn frequency'
)
concat render_settlement_type_picker
end
end
# Settlement type options
def render_settlement_type_picker
content_tag :div, class: 'settlement-picker', role: 'group', aria: {
label: 'Settlement types'
} do
SettlementType.all.each do |type|
concat content_tag :div, class: 'settlement-option' do
concat tag.label(class: 'settlement-label') do
concat tag.checkbox_tag(
'settlement_types',
value: type.id,
checked: true
)
concat content_tag(:span, type.name, class: 'settlement-name')
end
end
end
end
end
# Generic slider control with accessibility
#
# @param id [String] parameter identifier
# @param label [String] display label
# @param min [Integer] minimum value
# @param max [Integer] maximum value
# @param value [Integer] current value
# @param description [String] help text
# @param unit [String] unit suffix (optional)
def render_slider_control(id:, label:, min:, max:, value:, description:, unit: nil)
content_tag :div, class: 'slider-control', data: {
world_builder_target: 'slider',
slider_param: id
} do
concat content_tag(:label, label, for: id, class: 'slider-label')
concat content_tag(:p, description, class: 'slider-description', id: "#{id}_desc")
concat content_tag(:div, class: 'slider-input-container') do
concat tag.input(
type: 'range',
id: id,
name: id,
min: min,
max: max,
value: value,
aria: {
describedby: "#{id}_desc",
valuenow: value,
valuemin: min,
valuemax: max
},
data: {
action: 'world-builder#updatePreview',
preview_param: id
}
)
value_display = unit ? "#{value} #{unit}" value.to_s
concat content_tag(:span, value_display, class: 'slider-value', aria: {
hidden: true
})
end
end
end
# Preview panel with Turbo Frame live updates
def render_preview_panel
content_tag :section, class: 'preview-panel', aria: {
label: 'Generation Preview',
role: 'region'
}, data: {
world_builder_target: 'preview'
} do
concat content_tag(:h2, 'Live Preview', class: 'panel-title')
# Turbo Frame for incremental updates
concat turbo_frame_tag '/admin/worlds/preview', id: 'world-preview', data: {
world_builder_target: 'previewFrame',
params: @params.to_json
} do
concat render_preview_content
end
end
end
# Preview content (rendered inside Turbo Frame)
def render_preview_content
content_tag :div, class: 'preview-content' do
# Terrain heatmap visualization
concat render_terrain_preview
# Biome overlay
concat render_biome_overlay
# Resource markers
concat render_resource_markers
end
end
# Terrain preview heatmap
def render_terrain_preview
content_tag :div, class: 'terrain-preview', aria: {
label: 'Terrain elevation preview'
} do
# In production: Canvas-based rendering via Stimulus
# Server-side: SVG generation for progressive enhancement
concat tag.svg(class: 'terrain-svg', viewBox: '0 0 500 500') do
concat tag.defs
# Height-based path generation
concat render_elevation_paths
end
end
end
# Elevation path rendering
def render_elevation_paths
# Generate SVG paths based on elevation data
# In production: Cached preview from generation pipeline
(0..10).map do |level|
tag.path(
d: generate_elevation_path(level),
fill: elevation_color(level),
aria: { hidden: true }
)
end.join
end
def generate_elevation_path(level)
# Procedural path generation
'M 0,500 L 500,500 L 500,' + (50 - level * 5).to_s + ' L 0,' + (50 - level * 5).to_s + ' Z'
end
def elevation_color(level)
case level
when 0..2
'#4a90e2' # Water
when 3..5
'#7b68ee' # Plains
when 6..8
'#90a86b' # Hills
else
'#8b7d6b' # Mountains
end
end
# Biome overlay on terrain
def render_biome_overlay
content_tag :div, class: 'biome-overlay', aria: {
hidden: true
} do
@params[:biome_distribution]&.each do |biome_id|
biome = BiomeType.find(biome_id)
concat content_tag(:span, biome.icon, class: 'biome-indicator')
end
end
end
# Resource distribution markers
def render_resource_markers
content_tag :div, class: 'resource-markers', aria: {
label: 'Resource spawn locations'
} do
# Marker visualization for ore/vegetation spawns
concat content_tag(:p, "Resource density: #{@params[:resource_density]}%")
end
end
# Generation controls (Generate, Apply, Reset)
def render_generation_controls
content_tag :section, class: 'generation-controls', role: 'navigation' do
concat content_tag(:h2, 'Generation Controls', class: 'controls-title')
concat content_tag :div, class: 'control-buttons' do
concat link_to('Generate', '/admin/worlds/generate',
method: :post,
params: { world_id: @world_id, params: @params },
class: 'btn-primary',
data: {
world_builder_target: 'generateButton',
disable: 'true'
}
)
concat link_to('Apply Parameters', '/admin/worlds/apply',
method: :post,
params: { world_id: @world_id, params: @params },
class: 'btn-success'
)
concat link_to('Reset', '/admin/worlds/reset',
method: :post,
params: { world_id: @world_id },
class: 'btn-secondary'
)
end
# Progress indicator (Turbo Stream)
concat content_tag :div, class: 'generation-progress', data: {
world_builder_target: 'progress',
turbo_stream_source: 'generation'
}, aria: {
live: 'polite',
label: 'Generation progress'
} do
concat content_tag(:span, 'Ready', class: 'progress-status')
end
end
end
private
attr_reader :world_id, :params, :world
end
end