Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
SIMVX
HomeExamplesEditorDocs
SimVX documentation
SimVX documentation

Get started

  • Installation
  • Quickstart
  • Examples Gallery
  • Editor in the browser

Tutorials

  • Your first 2D game
  • Your first 3D game
  • From monolithic to composed
  • Building a scene in the editor

Guides: scenes and nodes

  • Node system
  • Scenes
  • Patterns
  • Event Bus
  • Save system

Guides: input and UI

  • Input
  • UI system
  • Fonts and languages

Guides: 2D

  • 2D drawing
  • TileMap
  • 2D lighting
  • Particles

Guides: 3D and rendering

  • Cameras (3D)
  • Materials
  • Custom shaders
  • WorldEnvironment
  • Compressed textures (BC / ASTC / ETC2)
  • Blender to SimVX (glTF)
  • Constructive solid geometry
  • Skeleton

Guides: gameplay systems

  • Animation
  • Physics
  • Navigation
  • 3D navigation
  • Audio
  • AssetServer
  • Package resources
  • Math types
  • LLM layer

Guides: testing and tools

  • Testing
  • Visual testing
  • Custom classes in the editor
  • IDE

Shipping

  • Shipping a game
  • Web export

Reference

  • API Reference
    • simvx.core
      • simvx.core.assets
        • simvx.core.assets.sources
          • simvx.core.assets.sources.mem
          • simvx.core.assets.sources.file
          • simvx.core.assets.sources.http
          • simvx.core.assets.sources.pkg
        • simvx.core.assets.loaders
          • simvx.core.assets.loaders.bytes_loader
          • simvx.core.assets.loaders.json_loader
          • simvx.core.assets.loaders.base
        • simvx.core.assets.server
        • simvx.core.assets.handle
        • simvx.core.assets.cache
        • simvx.core.assets.source
      • simvx.core.navigation
        • simvx.core.navigation.path_follower_2d
      • simvx.core.port_helpers
        • simvx.core.port_helpers.procedural_textures
      • simvx.core.ui
        • simvx.core.ui.ansi_parser
        • simvx.core.ui.splash
        • simvx.core.ui.rich_text
        • simvx.core.ui.advanced
        • simvx.core.ui.widgets
        • simvx.core.ui.touch_controller
        • simvx.core.ui.markers
        • simvx.core.ui.text_paragraph
        • simvx.core.ui.navigation
        • simvx.core.ui.testing
        • simvx.core.ui.overlay
        • simvx.core.ui.containers
        • simvx.core.ui.clipboard
        • simvx.core.ui.file_browser
        • simvx.core.ui.terminal
        • simvx.core.ui.theme
        • simvx.core.ui.syntax_highlighter
        • simvx.core.ui.tooltip
        • simvx.core.ui.core
        • simvx.core.ui.dock
        • simvx.core.ui.find_replace
        • simvx.core.ui.tabs
        • simvx.core.ui.tree
        • simvx.core.ui.split
        • simvx.core.ui.graph_edit
        • simvx.core.ui.virtual_controls
        • simvx.core.ui.virtual_scroll
        • simvx.core.ui.command_line
        • simvx.core.ui.completion_types
        • simvx.core.ui.file_dialog
        • simvx.core.ui.multiline
        • simvx.core.ui.focus
        • simvx.core.ui.button_group
        • simvx.core.ui.scroll
        • simvx.core.ui.toolbar
        • simvx.core.ui.menu
        • simvx.core.ui.grid_slots
        • simvx.core.ui.code_editor_panel
        • simvx.core.ui.marks
        • simvx.core.ui.autocomplete
        • simvx.core.ui.folding
        • simvx.core.ui.text_selection
        • simvx.core.ui.scene_host
        • simvx.core.ui.colour_picker
        • simvx.core.ui.enums
        • simvx.core.ui.code_edit
        • simvx.core.ui.types
        • simvx.core.ui.ui_input
      • simvx.core.text
        • simvx.core.text.measure
        • simvx.core.text.msdf
        • simvx.core.text.font
        • simvx.core.text.bundled
      • simvx.core.scene_io
        • simvx.core.scene_io.source
        • simvx.core.scene_io.symbols
        • simvx.core.scene_io.scene_file
        • simvx.core.scene_io.emitter
        • simvx.core.scene_io.layout
        • simvx.core.scene_io.scene_module
        • simvx.core.scene_io.loader
        • simvx.core.scene_io.detection
        • simvx.core.scene_io.syntax
      • simvx.core.input
        • simvx.core.input.tick
        • simvx.core.input.controller
        • simvx.core.input.engine
        • simvx.core.input.web_events
        • simvx.core.input.controls
        • simvx.core.input.listener
        • simvx.core.input.drag
        • simvx.core.input.state
        • simvx.core.input.players
        • simvx.core.input.router
        • simvx.core.input.map
        • simvx.core.input.spelling
        • simvx.core.input.gestures
        • simvx.core.input.prompts
        • simvx.core.input.capabilities
        • simvx.core.input.events
        • simvx.core.input.contexts
        • simvx.core.input.devices
        • simvx.core.input.actions
        • simvx.core.input.enums
        • simvx.core.input.layouts
      • simvx.core.testing
        • simvx.core.testing.scene_runner
        • simvx.core.testing.recorder
        • simvx.core.testing.optional
        • simvx.core.testing.selftest
        • simvx.core.testing.diagnostics
        • simvx.core.testing.benchmark
        • simvx.core.testing.input_sim
      • simvx.core.navigation3d
        • simvx.core.navigation3d.server
        • simvx.core.navigation3d.nodes
        • simvx.core.navigation3d.mesh
      • simvx.core.nodes_2d
        • simvx.core.nodes_2d.marker
        • simvx.core.nodes_2d.camera
        • simvx.core.nodes_2d.remote_transform
        • simvx.core.nodes_2d.trail
        • simvx.core.nodes_2d.ninepatch
        • simvx.core.nodes_2d.canvas
        • simvx.core.nodes_2d.shapes
        • simvx.core.nodes_2d.node2d
        • simvx.core.nodes_2d.spring_follow
        • simvx.core.nodes_2d.ysort
        • simvx.core.nodes_2d.text
        • simvx.core.nodes_2d.background_bands
        • simvx.core.nodes_2d.path
      • simvx.core.math
        • simvx.core.math.aabb
        • simvx.core.math.rect2
        • simvx.core.math.matrices
        • simvx.core.math.transforms
        • simvx.core.math.raycast
        • simvx.core.math.curves
        • simvx.core.math.types
      • simvx.core.graphics
        • simvx.core.graphics.material
        • simvx.core.graphics.texture
        • simvx.core.graphics.shader_bindings
        • simvx.core.graphics.texture_slot
        • simvx.core.graphics.mesh
        • simvx.core.graphics.shader
      • simvx.core.debug
        • simvx.core.debug.ui_inspector
        • simvx.core.debug.overlay
        • simvx.core.debug.profiler
      • simvx.core.lsp
        • simvx.core.lsp.protocol
        • simvx.core.lsp.server
        • simvx.core.lsp.client
      • simvx.core.animation
        • simvx.core.animation.tween
        • simvx.core.animation.skeletal
        • simvx.core.animation.state_machine
        • simvx.core.animation.sprite
        • simvx.core.animation.player
        • simvx.core.animation.track
        • simvx.core.animation.blend_space
      • simvx.core.nodes_3d
        • simvx.core.nodes_3d.chase_camera
        • simvx.core.nodes_3d.marker
        • simvx.core.nodes_3d.camera
        • simvx.core.nodes_3d.lights
        • simvx.core.nodes_3d.node3d
        • simvx.core.nodes_3d.remote_transform
        • simvx.core.nodes_3d.multimesh
        • simvx.core.nodes_3d.spring_arm
        • simvx.core.nodes_3d.sprite
        • simvx.core.nodes_3d.text
        • simvx.core.nodes_3d.mesh
        • simvx.core.nodes_3d.path
      • simvx.core.physics
        • simvx.core.physics.builtin
          • simvx.core.physics.builtin.world2d
          • simvx.core.physics.builtin.world
        • simvx.core.physics.pymunk_backend
        • simvx.core.physics.material
        • simvx.core.physics.shapes2d
        • simvx.core.physics.nodes
        • simvx.core.physics.capability
        • simvx.core.physics.shapes
        • simvx.core.physics.world2d
        • simvx.core.physics.slide
        • simvx.core.physics.root
        • simvx.core.physics.nodes2d
        • simvx.core.physics.backends
        • simvx.core.physics.query2d
        • simvx.core.physics.world
        • simvx.core.physics.query
      • simvx.core.audio_backend
      • simvx.core.audio_synth
      • simvx.core.mesh_builder
      • simvx.core.water
      • simvx.core.particle_trail
      • simvx.core.mesh_lod
      • simvx.core.scene_target_graph
      • simvx.core.git_status
      • simvx.core.background
      • simvx.core.gpu_particles
      • simvx.core.planar_reflection
      • simvx.core.gradient
      • simvx.core.scene_tree
      • simvx.core.event_bus
      • simvx.core.branding
      • simvx.core.clipboard
      • simvx.core.shortcuts
      • simvx.core.decal
      • simvx.core.i18n
      • simvx.core.atlas
      • simvx.core.hot_reload
      • simvx.core.ocean
      • simvx.core.colour
      • simvx.core.save_manager
      • simvx.core.run
      • simvx.core.descriptors
      • simvx.core.resource
      • simvx.core.env_sync_spec
      • simvx.core.project
      • simvx.core.file_state
      • simvx.core.audio_bus
      • simvx.core.clustered_lighting
      • simvx.core.easing
      • simvx.core.config
      • simvx.core.selection
      • simvx.core.rain
      • simvx.core.irradiance_volume
      • simvx.core.node
      • simvx.core.gesture
      • simvx.core.document
      • simvx.core.properties
      • simvx.core.undo
      • simvx.core.diagnostics
      • simvx.core.skeleton2d
      • simvx.core.gizmo
      • simvx.core.shell_node
      • simvx.core.decorators
      • simvx.core.light2d
      • simvx.core.mesh_instance_2d
      • simvx.core.audio_errors
      • simvx.core.procedural_sky
      • simvx.core.cli
      • simvx.core.audio_effect
      • simvx.core.signals
      • simvx.core.noise
      • simvx.core.skeleton
      • simvx.core.audio_listener
      • simvx.core.audio_protocol
      • simvx.core.audio
      • simvx.core.scene_step
      • simvx.core.py_console
      • simvx.core.reflection_probe
      • simvx.core.export
      • simvx.core.tilemap
      • simvx.core.asset_resolver
      • simvx.core.io
      • simvx.core.viewport
      • simvx.core.fog_volume
      • simvx.core.graphics_quality
      • simvx.core.render_view
      • simvx.core.csg
      • simvx.core.scripted_demo
      • simvx.core.particles
      • simvx.core.alignment
      • simvx.core.coroutines
      • simvx.core.colour_grading
      • simvx.core.world_environment
    • simvx.graphics
      • simvx.graphics.materials
        • simvx.graphics.materials.material
        • simvx.graphics.materials.texture
        • simvx.graphics.materials.custom_shader
        • simvx.graphics.materials.shader_compiler
      • simvx.graphics.assets
        • simvx.graphics.assets.block_decode
        • simvx.graphics.assets.image_loader
        • simvx.graphics.assets.ktx2_loader
        • simvx.graphics.assets.mesh_loader
        • simvx.graphics.assets.cubemap_loader
        • simvx.graphics.assets.scene_import
        • simvx.graphics.assets.dds_loader
      • simvx.graphics.gpu
        • simvx.graphics.gpu.memory
        • simvx.graphics.gpu.multi_device
        • simvx.graphics.gpu.instance
        • simvx.graphics.gpu.queue
        • simvx.graphics.gpu.context
        • simvx.graphics.gpu.pipeline_cache
        • simvx.graphics.gpu.sync
        • simvx.graphics.gpu.descriptors
        • simvx.graphics.gpu.retire
        • simvx.graphics.gpu.secondary_engine
        • simvx.graphics.gpu.timestamp_pool
        • simvx.graphics.gpu.pipeline_compute
        • simvx.graphics.gpu.device
        • simvx.graphics.gpu.swapchain
        • simvx.graphics.gpu.commands
        • simvx.graphics.gpu.capabilities
        • simvx.graphics.gpu.pipeline
      • simvx.graphics.ui
      • simvx.graphics.scene
        • simvx.graphics.scene.camera
        • simvx.graphics.scene.frustum
        • simvx.graphics.scene.draw_batch
      • simvx.graphics.shaders
        • simvx.graphics.shaders.variants
      • simvx.graphics.picking
        • simvx.graphics.picking.pick_pass
        • simvx.graphics.picking.raycast
      • simvx.graphics.renderer
        • simvx.graphics.renderer.sub_viewport
        • simvx.graphics.renderer.light_cull_pass
        • simvx.graphics.renderer.decal_pack
        • simvx.graphics.renderer.layer_post
        • simvx.graphics.renderer.draw2d_pass
        • simvx.graphics.renderer.bloom_pass
        • simvx.graphics.renderer.pass_helpers
        • simvx.graphics.renderer.viewport_manager
        • simvx.graphics.renderer.bindless_draw2d_pass
        • simvx.graphics.renderer.gizmo_pass
        • simvx.graphics.renderer.ssgi_pass
        • simvx.graphics.renderer.render_graph
        • simvx.graphics.renderer.pass_orchestrator
        • simvx.graphics.renderer.shadow_data
        • simvx.graphics.renderer.grid_pass
        • simvx.graphics.renderer.reflection_probe_pack
        • simvx.graphics.renderer.shadow_cache
        • simvx.graphics.renderer.hiz_pass
        • simvx.graphics.renderer.render_target
        • simvx.graphics.renderer.scene_renderer
        • simvx.graphics.renderer.shadow_renderer
        • simvx.graphics.renderer.ocean_pack
        • simvx.graphics.renderer.irradiance_volume_pass
        • simvx.graphics.renderer.render_context
        • simvx.graphics.renderer.resource_registry
        • simvx.graphics.renderer.depth_prepass
        • simvx.graphics.renderer.ssao_pass
        • simvx.graphics.renderer.particle_compute
        • simvx.graphics.renderer.environment_sync
        • simvx.graphics.renderer.game_viewport
        • simvx.graphics.renderer.water_pack
        • simvx.graphics.renderer.occlusion_cull
        • simvx.graphics.renderer.skybox_pass
        • simvx.graphics.renderer.ocean_compute
        • simvx.graphics.renderer.ibl_pass
        • simvx.graphics.renderer.velocity_pass
        • simvx.graphics.renderer.buffer_manager
        • simvx.graphics.renderer.ocean_fft
        • simvx.graphics.renderer.point_shadow_pass
        • simvx.graphics.renderer.forward
        • simvx.graphics.renderer.reflection_probe_pass
        • simvx.graphics.renderer.render_thread
        • simvx.graphics.renderer.outline_pass
        • simvx.graphics.renderer.tilemap_pass
        • simvx.graphics.renderer.particle_pass
        • simvx.graphics.renderer.text_pass
        • simvx.graphics.renderer.async_compute
        • simvx.graphics.renderer.pipeline_manager
        • simvx.graphics.renderer.gpu_batch
        • simvx.graphics.renderer.shadow_pass
        • simvx.graphics.renderer.light2d_pass
        • simvx.graphics.renderer.secondary_sru
        • simvx.graphics.renderer.vertex_layouts
        • simvx.graphics.renderer.transparency
        • simvx.graphics.renderer.view_occlusion
        • simvx.graphics.renderer.post_process
        • simvx.graphics.renderer.ssr_pass
        • simvx.graphics.renderer.mesh_registry
        • simvx.graphics.renderer.caster_slots
        • simvx.graphics.renderer.render_packet
        • simvx.graphics.renderer.scene_copy
        • simvx.graphics.renderer.billboard2d_data
        • simvx.graphics.renderer.render_pass
        • simvx.graphics.renderer.fog_volume_pack
        • simvx.graphics.renderer.billboard2d_pass
        • simvx.graphics.renderer.passes
        • simvx.graphics.renderer.overlay_renderer
        • simvx.graphics.renderer.water_pass
        • simvx.graphics.renderer.render_view
        • simvx.graphics.renderer.volumetric_fog_pass
        • simvx.graphics.renderer.custom_post_process
        • simvx.graphics.renderer.ocean_pass
        • simvx.graphics.renderer.colour_grading
        • simvx.graphics.renderer.shadow_math
        • simvx.graphics.renderer.taa_pass
        • simvx.graphics.renderer.tile_types
      • simvx.graphics.render2d
        • simvx.graphics.render2d.clip_scope
        • simvx.graphics.render2d.publish
        • simvx.graphics.render2d.submit
        • simvx.graphics.render2d.cache
        • simvx.graphics.render2d.item_list
        • simvx.graphics.render2d.item_builder
      • simvx.graphics.streaming
        • simvx.graphics.streaming.server
      • simvx.graphics.platform
      • simvx.graphics.playtest_patterns
      • simvx.graphics.text_renderer
      • simvx.graphics.draw2d_texture
      • simvx.graphics.draw2d_text
      • simvx.graphics.engine
      • simvx.graphics.testing
      • simvx.graphics.presentation
      • simvx.graphics.scene_adapter
      • simvx.graphics.engine_surface
      • simvx.graphics.blend
      • simvx.graphics.gpu_gate
      • simvx.graphics.draw2d_vertex
      • simvx.graphics.playtest
      • simvx.graphics.text_utils
      • simvx.graphics.cook
      • simvx.graphics.frame_loop
      • simvx.graphics.material_slots
      • simvx.graphics.pytest_plugin
      • simvx.graphics.draw2d_transform
      • simvx.graphics.cli
      • simvx.graphics.input_adapter
      • simvx.graphics.app
      • simvx.graphics.draw2d
      • simvx.graphics.draw2d_ops
      • simvx.graphics.debug_draw
      • simvx.graphics.frame_globals
      • simvx.graphics.types
    • simvx.editor
      • simvx.editor.panels
        • simvx.editor.panels.inspector_sections
        • simvx.editor.panels.scene_tree
          • simvx.editor.panels.scene_tree.type_registry
          • simvx.editor.panels.scene_tree.dialogs
          • simvx.editor.panels.scene_tree.panel
        • simvx.editor.panels.anchor_picker
        • simvx.editor.panels.repl_panel
        • simvx.editor.panels.asset_browser
        • simvx.editor.panels.inspector_widgets
        • simvx.editor.panels.animation
        • simvx.editor.panels.profiler_panel
        • simvx.editor.panels.scene3d_view
        • simvx.editor.panels.file_browser
        • simvx.editor.panels.play_controls
        • simvx.editor.panels.code_tab
        • simvx.editor.panels.animation_editor
        • simvx.editor.panels.properties
        • simvx.editor.panels.status_bar
        • simvx.editor.panels.section_widgets
        • simvx.editor.panels.console
        • simvx.editor.panels.scene2d_view
      • simvx.editor.testing
        • simvx.editor.testing.demo_steps
          • simvx.editor.testing.demo_steps.handlers
            • simvx.editor.testing.demo_steps.handlers.inspector
            • simvx.editor.testing.demo_steps.handlers.nodes
            • simvx.editor.testing.demo_steps.handlers.menu
            • simvx.editor.testing.demo_steps.handlers.viewport
          • simvx.editor.testing.demo_steps.steps
        • simvx.editor.testing.hosting
      • simvx.editor.command_palette
      • simvx.editor.refactor_inline
      • simvx.editor.autosave
      • simvx.editor.web_export
      • simvx.editor.dialog_message
      • simvx.editor.refactor_extract
      • simvx.editor.unsaved_class_warning_dialog
      • simvx.editor.export_dialog
      • simvx.editor.about_dialog
      • simvx.editor.main
      • simvx.editor.gizmo_manipulator
      • simvx.editor.make_custom_class_dialog
      • simvx.editor.error_nav
      • simvx.editor.theme
      • simvx.editor.play_mode
      • simvx.editor.error_recovery
      • simvx.editor.project_settings_dialog
      • simvx.editor.project
      • simvx.editor.node_ops
      • simvx.editor.workspace_tabs
      • simvx.editor.game_render_hook
      • simvx.editor.project_classes
      • simvx.editor.config
      • simvx.editor.scene_file_ops
      • simvx.editor.state
      • simvx.editor.rename_class_dialog
      • simvx.editor.layout_presets
      • simvx.editor.default_scenes
      • simvx.editor.convert_to_class
      • simvx.editor.welcome
      • simvx.editor.export_controller
      • simvx.editor.input_map_dialog
      • simvx.editor.modal_dialog
      • simvx.editor.project_registry
      • simvx.editor.preferences_dialog
      • simvx.editor.plugin
      • simvx.editor.cli
      • simvx.editor.menus
      • simvx.editor.root
      • simvx.editor.keyboard_nav
      • simvx.editor.scene_diff
      • simvx.editor.app
      • simvx.editor.live_file_ops
      • simvx.editor.save_dialog
      • simvx.editor.extract
      • simvx.editor.save_report_dialog
      • simvx.editor.hints
      • simvx.editor.templates
      • simvx.editor.commands
      • simvx.editor.duplicate_node_dialog
    • simvx.ide
      • simvx.ide.lint
        • simvx.ide.lint.runner
      • simvx.ide.panels
        • simvx.ide.panels.terminal_panel
        • simvx.ide.panels.output_panel
        • simvx.ide.panels.debug_panel
        • simvx.ide.panels.search_panel
        • simvx.ide.panels.file_browser
        • simvx.ide.panels.symbol_outline
        • simvx.ide.panels.problems_panel
        • simvx.ide.panels.settings_panel
      • simvx.ide.dap
        • simvx.ide.dap.client
      • simvx.ide.widgets
        • simvx.ide.widgets.command_palette
        • simvx.ide.widgets.minimap
        • simvx.ide.widgets.confirm_dialog
        • simvx.ide.widgets.status_bar
        • simvx.ide.widgets.goto_line
        • simvx.ide.widgets.autocomplete
      • simvx.ide.lsp
        • simvx.ide.lsp.client
      • simvx.ide.language_server
      • simvx.ide.editor_integration
      • simvx.ide.edit_actions
      • simvx.ide.playtest
      • simvx.ide.config
      • simvx.ide.state
      • simvx.ide.embedded
      • simvx.ide.app
      • simvx.ide.file_tabs
      • simvx.ide.keybindings
      • simvx.ide.debug_session
  • API cheat sheet
  • Core engine
  • Graphics
  • App
  • Physics Backends
  • Windowing backends
  • Architecture

Project

  • SimVX Documentation License
  • Building from source
  • Contributing
Back to top
View this page

menu.py¶

Part of Bloom.

  1"""Bloom menus: main menu (mode + difficulty) and the how-to-play card.
  2
  3Both are anchored, resize-aware UI built from SimVX widgets. The how-to card
  4runs a looping live demo of the one rule by reusing the real board, resolver
  5and :class:`HexBoardView`, so the tutorial can never drift from the game. The
  6root parents the card under a CanvasLayer, which pins it to the window.
  7"""
  8
  9import math
 10
 11import render
 12from ai import DIFFICULTY_BLURB
 13from board import CENTRE, Board, Owner, resolve_placement
 14from hex_grid import Hex, hex_corners, hex_in_range
 15from render import HexBoardView
 16
 17from simvx.core import (
 18    Anchor,
 19    Button,
 20    HBoxContainer,
 21    Label,
 22    Node2D,
 23    Signal,
 24    SizingMode,
 25    VBoxContainer,
 26    Vec2,
 27    tween,
 28    wait,
 29)
 30from simvx.core.animation.tween import ease_out_back
 31
 32TITLE = (1.0, 0.72, 0.42, 1.0)
 33TEXT = (0.93, 0.94, 0.97, 1.0)
 34TEXT_DIM = (0.62, 0.65, 0.74, 1.0)
 35BTN_BG = (0.16, 0.18, 0.24, 1.0)
 36BTN_HOVER = (0.22, 0.26, 0.34, 1.0)
 37BTN_DOWN = (0.12, 0.14, 0.19, 1.0)
 38SEL_BG = (0.30, 0.34, 0.46, 1.0)
 39PLAY_BG = (0.62, 0.34, 0.16, 1.0)
 40PLAY_HOVER = (0.74, 0.42, 0.20, 1.0)
 41
 42
 43def make_button(
 44    text, width=0.0, height=0.0, *, font=18.0, bg=BTN_BG, hover=BTN_HOVER, down=BTN_DOWN, fg=TEXT
 45) -> Button:
 46    """Themed :class:`Button` factory shared by every Bloom screen.
 47
 48    ``width`` / ``height`` may be left at 0 when a container sizes the button.
 49    """
 50    b = Button(text)
 51    b.size = Vec2(width, height)
 52    b.font_size = font
 53    b.bg_colour = bg
 54    b.hover_colour = hover
 55    b.pressed_colour = down
 56    b.text_colour = fg
 57    b.border_colour = (0.30, 0.33, 0.42, 1.0)
 58    return b
 59
 60
 61class _Segmented:
 62    """A row of mutually-exclusive option buttons."""
 63
 64    def __init__(self, options: list[tuple[str, str]], default: str, on_change):
 65        self.options = options
 66        self.value = default
 67        self._on_change = on_change
 68        self.buttons: dict[str, Button] = {}
 69        self.row = HBoxContainer()
 70        self.row.separation = 6
 71        self.row.sizing = SizingMode.FILL  # the row shares its width evenly, at any size
 72
 73    def build(self, total_w: float, h: float) -> HBoxContainer:
 74        for key, label in self.options:
 75            b = make_button(label, height=h, font=15.0)
 76            b.pressed.connect(lambda k=key: self.select(k))
 77            self.buttons[key] = b
 78            self.row.add_child(b)
 79        self.row.size = Vec2(total_w, h)
 80        self._restyle()
 81        return self.row
 82
 83    def select(self, key: str):
 84        if key == self.value:
 85            return
 86        self.value = key
 87        self._restyle()
 88        self._on_change(key)
 89
 90    def set_enabled(self, enabled: bool):
 91        for b in self.buttons.values():
 92            b.bg_colour = BTN_BG if enabled else (0.12, 0.12, 0.15, 1.0)
 93            b.text_colour = TEXT if enabled else TEXT_DIM
 94        if enabled:
 95            self._restyle()
 96
 97    def _restyle(self):
 98        for key, b in self.buttons.items():
 99            sel = key == self.value
100            b.bg_colour = SEL_BG if sel else BTN_BG
101            b.text_colour = (1.0, 1.0, 1.0, 1.0) if sel else TEXT
102
103
104class MainMenu(Node2D):
105    """Title, mode/difficulty selectors and Play. Emits configuration on start."""
106
107    start_requested = Signal(object)  # dict: {"mode", "difficulty"}
108    help_requested = Signal()
109    quit_requested = Signal()
110
111    def __init__(self, show_quit: bool = True, **kw):
112        super().__init__(name="MainMenu", **kw)
113        self._show_quit = show_quit
114        self.mode = "ai"
115        self.difficulty = "sharp"
116        self._mode_seg: _Segmented | None = None
117        self._diff_seg: _Segmented | None = None
118        self._diff_label: Label | None = None
119        self._root: VBoxContainer | None = None
120        self._enter = 0.0  # title entrance 0 -> 1 (eased), persists across resizes
121
122    def on_ready(self):
123        self._rebuild()
124        self.tree.screen_resized.connect(self._on_resize)
125        self.start_coroutine(tween(self, "_enter", 1.0, 0.55, easing=ease_out_back))
126
127    def on_update(self, dt):
128        self.queue_redraw()  # drawn title + living backdrop animate every frame
129
130    def _on_resize(self, *_):
131        if self._root is not None:
132            self._root.destroy()
133            self._root = None
134        self._rebuild()
135
136    def _screen_w(self) -> float:
137        return render.screen_wh(self.tree)[0]
138
139    def _rebuild(self):
140        w = self._screen_w()
141        col = max(280.0, min(w * 0.7, 440.0))
142        root = VBoxContainer(name="MenuCol", anchor=Anchor.CENTER, size=(col, 500))
143        root.separation = 12
144
145        # The "BLOOM" title is drawn in on_draw (animated glow + entrance); reserve
146        # its slot here so the column layout is unchanged.
147        root.add_child(self._spacer(col, 90))
148
149        tag = Label("Grow your colour. Starve theirs.")
150        tag.font_size = 15.0
151        tag.text_colour = TEXT_DIM
152        tag.alignment = "centre"
153        tag.size = Vec2(col, 22)
154        root.add_child(tag)
155
156        root.add_child(self._spacer(col, 10))
157
158        self._mode_seg = _Segmented([("ai", "vs Computer"), ("hotseat", "2 Players")], self.mode, self._set_mode)
159        root.add_child(self._mode_seg.build(col, 46))
160
161        self._diff_seg = _Segmented(
162            [("calm", "Calm"), ("sharp", "Sharp"), ("ruthless", "Ruthless")], self.difficulty, self._set_difficulty
163        )
164        root.add_child(self._diff_seg.build(col, 46))
165
166        self._diff_label = Label(DIFFICULTY_BLURB[self.difficulty])
167        self._diff_label.font_size = 13.0
168        self._diff_label.text_colour = TEXT_DIM
169        self._diff_label.alignment = "centre"
170        self._diff_label.size = Vec2(col, 20)
171        root.add_child(self._diff_label)
172
173        root.add_child(self._spacer(col, 8))
174
175        play = make_button(
176            "PLAY", col, 58, font=24.0, bg=PLAY_BG, hover=PLAY_HOVER, down=(0.50, 0.27, 0.12, 1.0), fg=(1, 1, 1, 1)
177        )
178        play.pressed.connect(self._on_play)
179        root.add_child(play)
180
181        how = make_button("How to play", col, 40, font=15.0)
182        how.pressed.connect(self.help_requested.emit)
183        root.add_child(how)
184
185        if self._show_quit:
186            quit_btn = make_button("Quit", col, 36, font=14.0, fg=TEXT_DIM)
187            quit_btn.pressed.connect(self.quit_requested.emit)
188            root.add_child(quit_btn)
189
190        self._apply_mode_state()
191        self.add_child(root)
192        self._root = root
193
194    def _spacer(self, w, h) -> Label:
195        s = Label("")
196        s.size = Vec2(w, h)
197        return s
198
199    def _set_mode(self, mode):
200        self.mode = mode
201        self._apply_mode_state()
202
203    def _set_difficulty(self, diff):
204        self.difficulty = diff
205        if self._diff_label:
206            self._diff_label.text = DIFFICULTY_BLURB[diff]
207
208    def _apply_mode_state(self):
209        if self._diff_seg:
210            self._diff_seg.set_enabled(self.mode == "ai")
211
212    def _on_play(self):
213        self.start_requested.emit({"mode": self.mode, "difficulty": self.difficulty})
214
215    def on_draw(self, renderer):
216        w, h = render.screen_wh(self.tree)
217        now = self.tree.now
218        # living backdrop: a few big, faint, slowly breathing blooms behind the menu
219        blooms = (
220            (w * 0.16, h * 0.32, render.WARM, 0.0),
221            (w * 0.85, h * 0.62, render.COOL, 2.1),
222            (w * 0.52, h * 0.9, render.WARM, 4.0),
223        )
224        plen = min(w, h) * 0.13
225        for bx, by, col, ph in blooms:
226            breath = 1.0 + 0.10 * math.sin(now * 0.5 + ph)
227            render.draw_flower(renderer, bx, by, plen * breath, (col[0], col[1], col[2], 0.04))
228        # the hero title: entrance scale/fade + gentle breathing + a soft warm glow
229        e = max(0.0, min(1.0, self._enter))
230        cx, cy = w / 2.0, h / 2.0 - 205.0
231        px = max(54.0, min(w * 0.10, 96.0))
232        breath = 1.0 + 0.02 * math.sin(now * 1.7)
233        sc = (px / 16.0) * (0.9 + 0.1 * self._enter) * breath
234        # soft 3-ring additive aura (falloff, not a hard orb)
235        glow = (1.0, 0.66, 0.34)
236        gb = 1.0 + 0.05 * math.sin(now * 1.7)
237        for gr, ga in ((0.55, 0.14), (0.95, 0.08), (1.4, 0.04)):
238            render.draw_disc(renderer, cx, cy, px * gr * gb, (glow[0], glow[1], glow[2], ga * e))
239        renderer.text(
240            "BLOOM",
241            fill=(TITLE[0], TITLE[1], TITLE[2], e),
242            scale=sc,
243            align="centre",
244            valign="centre",
245            rect=(0, cy - px, w, 2 * px),
246        )
247
248
249def demo_rect(tree) -> tuple[float, float, float]:
250    """The how-to card's mini-board rect as ``(x, y, side)``, from the live viewport."""
251    w, h = render.screen_wh(tree)
252    side = min(w * 0.6, h * 0.32)
253    return w / 2 - side / 2, h * 0.17, side
254
255
256class RuleDemo(Node2D):
257    """The looping live demo of the one rule, inside the how-to card.
258
259    Its own node because it is the one part of the card that belongs in the
260    post-processed lane: owned cores sit above 1.0 so the garden's bloom catches
261    them here exactly as it does on the real board, and ``post_processed=True``
262    asks for that lane. Placement still comes from the CanvasLayer above the card, so
263    this draws in window coordinates like everything else on it.
264    """
265
266    def __init__(self, **kw):
267        super().__init__(name="RuleDemo", post_processed=True, **kw)
268        self.board = Board(radius=1)
269        self.view = HexBoardView()
270
271    def on_ready(self):
272        self._reset()
273        self.start_coroutine(self._loop())
274
275    def on_update(self, dt):
276        self.queue_redraw()  # the board cells are plain state, not Properties
277
278    def _reset(self):
279        self.board.cells = dict.fromkeys(hex_in_range(CENTRE, 1), Owner.EMPTY)
280        self.board.cells[CENTRE] = Owner.COOL
281        self.board.cells[Hex(1, 0)] = Owner.WARM
282        self.board.last_placed = None
283        self.view.sync_static(self.board)
284
285    def _loop(self):
286        while True:
287            yield from wait(1.1)
288            seed = Hex(-1, 1)
289            if self.board.cells.get(seed) is Owner.EMPTY:
290                resolve_placement(self.board, seed, Owner.WARM)
291                self.view.sync_static(self.board)
292            yield from wait(1.6)
293            self._reset()
294
295    def on_draw(self, renderer):
296        x, y, side = demo_rect(self.tree)
297        self.view.layout(self.board, x, y, side, side)
298        self.view.draw(renderer, self.board)
299
300
301class HowToCard(Node2D):
302    """The how-to screen: dimming scrim, rule text, legend and a live demo.
303
304    The root parents this under a CanvasLayer, so everything it draws is pinned
305    to the window and composited after tonemap, clear of the board's bloom and
306    grain. The live demo is the one child that opts back into that lane.
307    """
308
309    back_requested = Signal()
310
311    def on_ready(self):
312        self.add_child(RuleDemo())
313        back = make_button("Back", font=18.0)
314        back.size = Vec2(300, 48)
315        back.inset = (0, 16)
316        back.anchor = Anchor.CENTER_BOTTOM
317        back.pressed.connect(self.back_requested.emit)
318        self.add_child(back)
319
320    def on_update(self, dt):
321        self.queue_redraw()  # the card re-fits the viewport every frame
322
323    def on_draw(self, renderer):
324        w, h = render.screen_wh(self.tree)
325        renderer.rect((0, 0), (w, h), fill=(0.04, 0.05, 0.07, 0.82))
326        renderer.text("How to play", (w / 2, h * 0.10), fill=TITLE, scale=1.8, align="centre")
327        _, demo_top, side = demo_rect(self.tree)
328        # legend row, centred under the demo
329        ly = demo_top + side + 26
330        self._legend(renderer, w / 2 - 150, ly, render.WARM, "You")
331        self._legend(renderer, w / 2 - 24, ly, render.COOL, "Opponent")
332        self._legend(renderer, w / 2 + 118, ly, render.EMPTY, "Empty")
333        # rule text, width-fitted so it never overflows on phones
334        rules = [
335            ("Press an empty hex to aim; release to place a seed.", TEXT, 0.95),
336            ("Capture rule: count only the cells TOUCHING a hex. An enemy hex flips", TEXT_DIM, 0.9),
337            ("when your seeds touching it outnumber its own. Then captures ripple out.", TEXT_DIM, 0.9),
338            ("Your hexes flip the same way on their turn, so keep yours connected.", TEXT_DIM, 0.9),
339        ]
340        for i, (line, colour, scale) in enumerate(rules):
341            renderer.text(
342                line,
343                fill=colour,
344                scale=scale,
345                align="centre",
346                rect=(20, ly + 26 + i * 22, w - 40, 22),
347                fit_to_width=True,
348            )
349
350    def _legend(self, renderer, x, y, colour, text):
351        renderer.polygon([(c.x, c.y) for c in hex_corners(Vec2(x, y), 10)], fill=colour)
352        renderer.text(text, (x + 16, y), fill=TEXT, scale=0.8, align="left", valign="centre")
Copyright © 2026, SimVX Contributors
Made with Sphinx and @pradyunsg's Furo