diff --git a/CONTROLS.md b/CONTROLS.md
new file mode 100644
index 0000000..9e18622
--- /dev/null
+++ b/CONTROLS.md
@@ -0,0 +1,33 @@
+# OpenIsopix Controls
+
+## Camera Controls
+- **Arrow Keys** - Move camera around the world
+- **Q/E** - Rotate camera view (4 directions: N, E, S, W)
+- **P** - Change pitch angle (cycle through 30°, 45°, 60°, 90°)
+- **Mouse Wheel / +/-** - Zoom in/out
+
+## Block Selection (1-5 Keys)
+- **1** - Select Grass block
+- **2** - Select Stone block
+- **3** - Select Water block
+- **4** - Select Wood block
+- **5** - Select Soil block
+
+## Interaction Modes (Space to Cycle)
+- **SELECT Mode** - Click blocks to query/select
+- **PLACE Mode** - Click to place selected block type
+- **REMOVE Mode** - Click to remove blocks
+- **QUERY Mode** - Click to get block information
+
+## Mouse Actions
+- **Left Click** - Place block (when in PLACE mode) or Select/Query
+- **Right Click** - Remove block (when in REMOVE mode)
+
+## Special Features
+- **F** - Toggle Fog of War (shows/hides explored areas)
+
+## Tips
+1. Use **Space** to cycle between interaction modes
+2. Select block type with **1-5** before placing
+3. Right-click to quickly remove unwanted blocks
+4. Use **Q/E** to rotate and view from different angles
diff --git a/assets/blocks/isometric-grass.svg b/assets/blocks/isometric-grass.svg
new file mode 100644
index 0000000..51e632e
--- /dev/null
+++ b/assets/blocks/isometric-grass.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/blocks/isometric-grass.svg.import b/assets/blocks/isometric-grass.svg.import
new file mode 100644
index 0000000..983a847
--- /dev/null
+++ b/assets/blocks/isometric-grass.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://jd0e4pm36sth"
+path="res://.godot/imported/isometric-grass.svg-c7f0d58de74f553c43ff0d8fa326480d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/blocks/isometric-grass.svg"
+dest_files=["res://.godot/imported/isometric-grass.svg-c7f0d58de74f553c43ff0d8fa326480d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/blocks/isometric-soil.svg b/assets/blocks/isometric-soil.svg
new file mode 100644
index 0000000..d2f09aa
--- /dev/null
+++ b/assets/blocks/isometric-soil.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/blocks/isometric-soil.svg.import b/assets/blocks/isometric-soil.svg.import
new file mode 100644
index 0000000..1fb8663
--- /dev/null
+++ b/assets/blocks/isometric-soil.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqtcsxq50k0im"
+path="res://.godot/imported/isometric-soil.svg-df29826697cd6ad1a28c2b8095c7e1fd.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/blocks/isometric-soil.svg"
+dest_files=["res://.godot/imported/isometric-soil.svg-df29826697cd6ad1a28c2b8095c7e1fd.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/blocks/isometric-stone.svg b/assets/blocks/isometric-stone.svg
new file mode 100644
index 0000000..2040822
--- /dev/null
+++ b/assets/blocks/isometric-stone.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/blocks/isometric-stone.svg.import b/assets/blocks/isometric-stone.svg.import
new file mode 100644
index 0000000..e87419f
--- /dev/null
+++ b/assets/blocks/isometric-stone.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c3v4mwol1m174"
+path="res://.godot/imported/isometric-stone.svg-e3e8771d77841634b3ec647f7ae6f1ee.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/blocks/isometric-stone.svg"
+dest_files=["res://.godot/imported/isometric-stone.svg-e3e8771d77841634b3ec647f7ae6f1ee.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/blocks/isometric-water.svg b/assets/blocks/isometric-water.svg
new file mode 100644
index 0000000..9ab0e93
--- /dev/null
+++ b/assets/blocks/isometric-water.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/blocks/isometric-water.svg.import b/assets/blocks/isometric-water.svg.import
new file mode 100644
index 0000000..9ae51a1
--- /dev/null
+++ b/assets/blocks/isometric-water.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://tyveqw0qdm4x"
+path="res://.godot/imported/isometric-water.svg-22d3c45208538c9115ac6824dc639ebc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/blocks/isometric-water.svg"
+dest_files=["res://.godot/imported/isometric-water.svg-22d3c45208538c9115ac6824dc639ebc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/blocks/isometric-wood.svg b/assets/blocks/isometric-wood.svg
new file mode 100644
index 0000000..ec7b4c1
--- /dev/null
+++ b/assets/blocks/isometric-wood.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/blocks/isometric-wood.svg.import b/assets/blocks/isometric-wood.svg.import
new file mode 100644
index 0000000..546dd6f
--- /dev/null
+++ b/assets/blocks/isometric-wood.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cjngmjk8bvb38"
+path="res://.godot/imported/isometric-wood.svg-b1373981494e38c702b8c03d2116c4e9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/blocks/isometric-wood.svg"
+dest_files=["res://.godot/imported/isometric-wood.svg-b1373981494e38c702b8c03d2116c4e9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/docs/API.md b/docs/API.md
new file mode 100644
index 0000000..e54707e
--- /dev/null
+++ b/docs/API.md
@@ -0,0 +1,376 @@
+# OpenIsopix API Documentation
+
+## Overview
+
+OpenIsopix provides a complete API for building 2D isometric block-based worlds with pixel-art graphics. The system is fully decoupled from UI, allowing easy integration into any game or application.
+
+## Core Components
+
+### 1. WorldAPI (`scripts/core/WorldAPI.gd`)
+
+The main interface for world manipulation. All world interactions should go through this API.
+
+#### Key Methods
+
+```gdscript
+# Register a new block type
+func register_block_type(block_type: BlockType) -> void
+
+# Add a block to the world
+func add_block(world_pos: Vector3i, type_id: String, height: float = 1.0) -> BlockInstance
+
+# Remove a block from the world
+func remove_block(world_pos: Vector3i) -> void
+
+# Get a block at world position
+func get_block(world_pos: Vector3i) -> BlockInstance
+
+# Modify a block's properties
+func modify_block(world_pos: Vector3i, property: String, value: Variant) -> void
+
+# Query block attributes
+func get_block_attribute(world_pos: Vector3i, attribute: String) -> Variant
+
+# Chunk management
+func load_chunk(chunk_pos: Vector2i) -> Chunk
+func unload_chunk(chunk_pos: Vector2i) -> void
+func get_chunk_blocks(chunk_pos: Vector2i) -> Array[BlockInstance]
+
+# Trigger events
+func trigger_block_click(world_pos: Vector3i) -> void
+func trigger_block_walk_over(world_pos: Vector3i) -> void
+```
+
+#### Signals
+
+```gdscript
+signal block_added(position: Vector3i, block: BlockInstance)
+signal block_removed(position: Vector3i)
+signal block_modified(position: Vector3i, block: BlockInstance)
+signal block_clicked(position: Vector3i, block: BlockInstance)
+signal block_walked_over(position: Vector3i, block: BlockInstance)
+signal chunk_loaded(chunk_pos: Vector2i)
+signal chunk_unloaded(chunk_pos: Vector2i)
+signal lighting_changed(position: Vector3i, light_level: float)
+```
+
+#### Example Usage
+
+```gdscript
+# Get reference to WorldAPI
+var world_api = get_node("/root/Main/WorldAPI")
+
+# Create a custom block type
+var custom_block = BlockType.new("my_block", "My Custom Block")
+custom_block.is_solid = true
+custom_block.emits_light = true
+custom_block.light_color = Color.CYAN
+world_api.register_block_type(custom_block)
+
+# Place blocks
+world_api.add_block(Vector3i(0, 0, 0), "grass")
+world_api.add_block(Vector3i(1, 0, 0), "my_block")
+
+# Query a block
+var block = world_api.get_block(Vector3i(0, 0, 0))
+if block:
+ print("Block type: ", block.get_type_id())
+ print("Block HP: ", block.hp)
+
+# Listen for events
+world_api.block_added.connect(_on_block_added)
+func _on_block_added(pos: Vector3i, block: BlockInstance):
+ print("Block added at ", pos)
+```
+
+### 2. BlockType (`scripts/core/BlockType.gd`)
+
+Defines a block type with all its properties.
+
+#### Properties
+
+```gdscript
+# Identification
+@export var id: String
+@export var display_name: String
+
+# Behavior flags
+@export var is_solid: bool = true
+@export var is_opaque: bool = true
+@export var is_climbable: bool = false
+@export var is_walkable: bool = true
+
+# Visual attributes
+@export var texture: Texture2D
+@export var animated: bool = false
+@export var animation_frames: Array[Texture2D]
+@export var emits_light: bool = false
+@export var light_color: Color = Color.WHITE
+@export var light_intensity: float = 1.0
+@export var light_radius: float = 100.0
+
+# Numeric attributes
+@export var max_hp: float = 100.0
+@export var material_type: String = "generic"
+@export var hardness: float = 1.0
+
+# Interaction
+@export var on_click_enabled: bool = true
+@export var on_walk_over_enabled: bool = false
+```
+
+### 3. BlockInstance (`scripts/core/BlockInstance.gd`)
+
+Represents a single block in the world.
+
+#### Properties
+
+```gdscript
+var position: Vector3i # World position
+var block_type: BlockType # Type reference
+var hp: float # Current health
+var height: float = 1.0 # Height (0.5 or 1.0)
+var light_level: float = 1.0 # Lighting (0.0 to 1.0)
+var is_revealed: bool = false # Fog of war state
+var custom_data: Dictionary # Extensible properties
+```
+
+#### Methods
+
+```gdscript
+func take_damage(amount: float) -> bool # Returns true if destroyed
+func heal(amount: float) -> void
+func is_destroyed() -> bool
+func get_type_id() -> String
+```
+
+### 4. Chunk (`scripts/core/Chunk.gd`)
+
+Manages a 16x16 region of blocks.
+
+#### Constants
+
+```gdscript
+const CHUNK_SIZE = 16
+```
+
+#### Methods
+
+```gdscript
+func set_block(local_pos: Vector3i, block: BlockInstance) -> void
+func get_block(local_pos: Vector3i) -> BlockInstance
+func remove_block(local_pos: Vector3i) -> void
+func to_world_pos(local_pos: Vector3i) -> Vector3i
+func get_all_blocks() -> Array[BlockInstance]
+```
+
+### 5. IsometricCamera (`scripts/rendering/IsometricCamera.gd`)
+
+Controls camera POV with rotation, zoom, and pitch.
+
+#### Enums
+
+```gdscript
+enum Heading { NORTH, EAST, SOUTH, WEST }
+enum PitchLevel { LOW, NORMAL, HIGH, TOP_DOWN }
+```
+
+#### Methods
+
+```gdscript
+func rotate_heading(clockwise: bool = true) -> void
+func cycle_pitch() -> void
+func zoom_camera(zoom_in: bool) -> void
+func set_camera_position(new_position: Vector2) -> void
+func world_to_iso(world_pos: Vector3) -> Vector2
+func screen_to_world(screen_pos: Vector2) -> Vector3
+```
+
+#### Signals
+
+```gdscript
+signal heading_changed(new_heading: Heading)
+signal pitch_changed(new_pitch: PitchLevel)
+signal zoom_changed(new_zoom: float)
+```
+
+### 6. LightingSystem (`scripts/systems/LightingSystem.gd`)
+
+Manages environmental and block-emitted lighting.
+
+#### Enums
+
+```gdscript
+enum EnvironmentalLevel {
+ PITCH_BLACK, VERY_DARK, DARK, DIM,
+ NORMAL, BRIGHT, VERY_BRIGHT
+}
+```
+
+#### Methods
+
+```gdscript
+func set_environmental_level(level: EnvironmentalLevel) -> void
+func calculate_block_lighting(world_pos: Vector3i) -> float
+```
+
+#### Signals
+
+```gdscript
+signal lighting_updated(position: Vector3i, light_level: float)
+```
+
+### 7. FogOfWarSystem (`scripts/systems/FogOfWarSystem.gd`)
+
+Manages map revelation and exploration.
+
+#### Methods
+
+```gdscript
+func reveal_area(center: Vector3i, radius: float = -1.0) -> void
+func is_revealed(world_pos: Vector3i) -> bool
+func hide_area(center: Vector3i, radius: float = -1.0) -> void
+func reveal_all() -> void
+func hide_all() -> void
+```
+
+#### Signals
+
+```gdscript
+signal area_revealed(center: Vector3i, radius: float)
+signal block_revealed(position: Vector3i)
+```
+
+### 8. InteractionSystem (`scripts/systems/InteractionSystem.gd`)
+
+Handles user input and world interaction.
+
+#### Enums
+
+```gdscript
+enum InteractionMode { SELECT, PLACE, REMOVE, QUERY }
+```
+
+#### Methods
+
+```gdscript
+func cycle_mode() -> void
+func set_mode(mode: InteractionMode) -> void
+```
+
+#### Signals
+
+```gdscript
+signal block_selected(position: Vector3i)
+signal block_hovered(position: Vector3i)
+signal interaction_mode_changed(mode: InteractionMode)
+```
+
+## Complete Example: Creating a Custom Game
+
+```gdscript
+extends Node2D
+
+var world_api: WorldAPI
+var camera: IsometricCamera
+var lighting: LightingSystem
+
+func _ready():
+ # Get system references
+ world_api = $WorldAPI
+ camera = $IsometricCamera
+ lighting = $LightingSystem
+
+ # Register custom blocks
+ _register_custom_blocks()
+
+ # Generate world
+ _generate_world()
+
+ # Setup game logic
+ world_api.block_clicked.connect(_on_block_clicked)
+
+func _register_custom_blocks():
+ # Create a glowing crystal block
+ var crystal = BlockType.new("crystal", "Magic Crystal")
+ crystal.is_solid = true
+ crystal.is_opaque = false
+ crystal.emits_light = true
+ crystal.light_color = Color(0.5, 0.0, 1.0)
+ crystal.light_intensity = 1.5
+ crystal.max_hp = 50.0
+ world_api.register_block_type(crystal)
+
+ # Create a trap block
+ var trap = BlockType.new("trap", "Spike Trap")
+ trap.is_solid = true
+ trap.on_walk_over_enabled = true
+ world_api.register_block_type(trap)
+ world_api.block_walked_over.connect(_on_trap_activated)
+
+func _generate_world():
+ # Create a 20x20 world
+ for x in range(-10, 10):
+ for z in range(-10, 10):
+ world_api.add_block(Vector3i(x, 0, z), "grass")
+
+ # Add some crystals
+ world_api.add_block(Vector3i(0, 1, 0), "crystal")
+ world_api.add_block(Vector3i(5, 1, 5), "crystal")
+
+ # Set darker environment
+ lighting.set_environmental_level(LightingSystem.EnvironmentalLevel.DARK)
+
+func _on_block_clicked(pos: Vector3i, block: BlockInstance):
+ if block.get_type_id() == "crystal":
+ print("Collected magic crystal!")
+ world_api.remove_block(pos)
+
+func _on_trap_activated(pos: Vector3i, block: BlockInstance):
+ print("Player took damage from trap!")
+```
+
+## Architecture Notes
+
+### Decoupling
+
+The system is designed with clear separation:
+- **Core**: Data structures and world management (no rendering)
+- **Rendering**: Visual representation (uses core data)
+- **Systems**: Game logic (lighting, fog, interaction)
+- **UI**: User interface (optional, demo only)
+
+### Extensibility
+
+Add new features by:
+1. Creating new `BlockType` definitions
+2. Using `custom_data` dictionary in `BlockInstance`
+3. Connecting to WorldAPI signals
+4. Extending systems (create new nodes)
+
+### Performance
+
+- Chunk-based loading (16x16 blocks per chunk)
+- Incremental lighting updates (100 blocks per frame)
+- Sprite caching for rendered blocks
+- Z-index based rendering order
+
+## Input Mapping
+
+Default input actions (defined in `project.godot`):
+
+- `ui_left/right/up/down`: Camera movement
+- `zoom_in/out`: Camera zoom
+- `rotate_left/right`: Camera rotation (Q/E)
+- `change_pitch`: Cycle pitch levels (P)
+- `place_block`: Place block (LMB)
+- `remove_block`: Remove block (RMB)
+- `toggle_fog`: Toggle fog reveal (F)
+
+## Next Steps
+
+1. **Add Custom Blocks**: Create new `BlockType` resources
+2. **Implement Game Logic**: Connect to WorldAPI signals
+3. **Create Assets**: Replace placeholder textures with pixel art
+4. **Extend Systems**: Add entities, AI, procedural generation
+5. **Optimize**: Profile and optimize for target platform
diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md
new file mode 100644
index 0000000..52f53b2
--- /dev/null
+++ b/docs/DEBUGGING.md
@@ -0,0 +1,362 @@
+# OpenIsopix - Debugging & Testing Guide
+
+## Project Setup
+
+### Requirements
+
+- Godot Engine 4.3 or later
+- Windows/Linux/macOS
+
+### Opening the Project
+
+1. Clone or download the repository
+2. Open Godot Engine
+3. Click "Import" and select the `project.godot` file
+4. Click "Import & Edit"
+
+### Running the Demo
+
+1. Press F5 or click the "Play" button
+2. The main scene (`scenes/Main.tscn`) will launch automatically
+
+## Debug Tools
+
+### Console Output
+
+The demo prints useful debug information:
+- Block placement/removal confirmations
+- Interaction mode changes
+- Block queries (position, type, attributes)
+- System initialization messages
+
+### In-Game Debug Commands
+
+Press the following keys to test features:
+
+1. **World Manipulation**
+ - `1-4`: Select block type (Grass/Stone/Water/Torch)
+ - `LMB`: Place selected block
+ - `RMB`: Remove block at cursor
+ - `Space`: Cycle interaction modes (SELECT/PLACE/REMOVE/QUERY)
+
+2. **Camera Controls**
+ - `Arrow Keys`: Pan camera
+ - `Q/E`: Rotate heading (90° increments)
+ - `P`: Cycle pitch (LOW/NORMAL/HIGH/TOP_DOWN)
+ - `+/-` or `Mouse Wheel`: Zoom in/out
+
+3. **System Testing**
+ - `F`: Reveal fog of war around cursor
+ - Click blocks in QUERY mode: Print block attributes
+
+### Query Mode
+
+Set interaction mode to QUERY (press Space until mode shows "QUERY"), then click any block to see:
+
+```
+=== Block Query ===
+Position: (x, y, z)
+Type: block_type_id
+HP: current/max
+Height: 0.5 or 1.0
+Light Level: 0.0 to 1.0
+Revealed: true/false
+Solid: true/false
+Opaque: true/false
+Climbable: true/false
+Emits Light: true/false
+==================
+```
+
+## Testing Scenarios
+
+### 1. Block Management
+
+**Test: Add and Remove Blocks**
+```
+1. Press '1' to select Grass
+2. Left-click empty spaces to place grass blocks
+3. Right-click placed blocks to remove them
+4. Press '2' to select Stone, repeat
+```
+
+**Expected**: Blocks appear/disappear correctly, world updates immediately
+
+### 2. Chunk System
+
+**Test: Chunk Loading**
+```
+1. Move camera far from origin (use arrow keys)
+2. Place blocks in different areas
+3. Return to origin
+```
+
+**Expected**: Blocks persist, chunks load/unload seamlessly
+
+### 3. Lighting System
+
+**Test: Light Sources**
+```
+1. Press '4' to select Torch
+2. Place torches in different locations
+3. Observe lighting on nearby blocks
+4. Remove torches and watch lighting update
+```
+
+**Expected**:
+- Torches emit yellow-orange light
+- Light attenuates with distance
+- Blocks in darkness are darker
+- Lighting updates when torches are added/removed
+
+### 4. Fog of War
+
+**Test: Map Revelation**
+```
+1. Notice some areas are semi-transparent (unrevealed)
+2. Press 'F' to reveal area around cursor
+3. Move camera to unrevealed areas
+4. Press 'F' to reveal more
+```
+
+**Expected**:
+- Unrevealed blocks are transparent/dim
+- Revealed blocks are fully visible
+- Revelation persists
+
+### 5. Camera System
+
+**Test: Point of View Changes**
+```
+1. Press 'Q' repeatedly - world rotates counter-clockwise
+2. Press 'E' repeatedly - world rotates clockwise
+3. Press 'P' repeatedly - camera pitch changes (4 levels)
+4. Use mouse wheel or +/- to zoom
+```
+
+**Expected**:
+- Each rotation is 90° (4 total headings)
+- Pitch changes vertical viewing angle
+- Zoom maintains center point
+- Movement controls adjust to rotation
+
+### 6. Interaction Modes
+
+**Test: Mode Switching**
+```
+1. Press Space to cycle modes: SELECT → PLACE → REMOVE → QUERY
+2. Observe cursor highlight color changes:
+ - SELECT: Yellow
+ - PLACE: Green
+ - REMOVE: Red
+ - QUERY: Blue
+```
+
+**Expected**: Mode changes reflected in cursor and behavior
+
+### 7. Block Heights
+
+**Test: Multi-Level Building**
+```
+1. Place a block at ground level (y=0)
+2. Click the same position - new block appears on top (y=1)
+3. Continue clicking to stack blocks
+4. Right-click to remove top blocks first
+```
+
+**Expected**: Blocks stack vertically, removal works top-to-bottom
+
+## Common Issues & Solutions
+
+### Issue: Blocks Not Appearing
+
+**Possible Causes:**
+1. Camera too far from world origin
+2. Blocks placed outside visible area
+3. Renderer not connected to WorldAPI
+
+**Solution:**
+- Reset camera position to (0, 0)
+- Check console for errors
+- Verify `renderer.world_api` is set in Main scene
+
+### Issue: Input Not Working
+
+**Possible Causes:**
+1. UI overlay capturing input
+2. Wrong scene running
+3. Input map not configured
+
+**Solution:**
+- Ensure Main.tscn is running
+- Check Project Settings → Input Map
+- Verify mouse is not over UI panels
+
+### Issue: Lighting Not Updating
+
+**Possible Causes:**
+1. LightingSystem not processing
+2. Too many blocks causing queue backlog
+3. WorldAPI not connected to lighting system
+
+**Solution:**
+- Check `lighting_system.world_api` reference
+- Reduce world size for testing
+- Verify signals are connected
+
+### Issue: Fog Not Working
+
+**Possible Causes:**
+1. FogSystem not initialized
+2. Starting area not revealed
+3. Fog disabled in settings
+
+**Solution:**
+- Check `_generate_demo_world()` calls `fog_system.reveal_area()`
+- Manually press 'F' to reveal
+- Verify `fog_system.world_api` is set
+
+## Unit Testing
+
+### Manual Test Checklist
+
+Create `tests/manual_tests.md` and track:
+
+- [ ] Place blocks of each type
+- [ ] Remove blocks
+- [ ] Rotate camera all 4 directions
+- [ ] Test all 4 pitch levels
+- [ ] Zoom in to max
+- [ ] Zoom out to min
+- [ ] Place torch and verify lighting
+- [ ] Remove torch and verify lighting updates
+- [ ] Reveal fog of war
+- [ ] Query block in QUERY mode
+- [ ] Stack blocks vertically
+- [ ] Move camera to chunk boundaries
+- [ ] Place blocks across multiple chunks
+- [ ] Test all interaction modes
+
+### Automated Testing (Future)
+
+For automated tests, use Godot's GUT framework:
+
+```gdscript
+# Example test structure
+extends GutTest
+
+var world_api: WorldAPI
+
+func before_each():
+ world_api = WorldAPI.new()
+ add_child(world_api)
+
+func test_add_block():
+ var block = world_api.add_block(Vector3i(0, 0, 0), "grass")
+ assert_not_null(block)
+ assert_eq(block.get_type_id(), "grass")
+
+func test_remove_block():
+ world_api.add_block(Vector3i(0, 0, 0), "grass")
+ world_api.remove_block(Vector3i(0, 0, 0))
+ var block = world_api.get_block(Vector3i(0, 0, 0))
+ assert_null(block)
+```
+
+## Performance Profiling
+
+### Using Godot Profiler
+
+1. Run project (F5)
+2. Go to Debugger tab → Profiler
+3. Monitor key metrics:
+ - **FPS**: Should stay above 60
+ - **Process Time**: Watch for spikes
+ - **Physics Time**: Should be minimal (no physics used)
+ - **Script Functions**: Check WorldAPI and Renderer calls
+
+### Optimization Checkpoints
+
+Monitor these values during gameplay:
+
+- **Block Count**: < 10,000 for smooth performance
+- **Chunk Count**: Only nearby chunks loaded (determined by `chunk_load_distance`)
+- **Sprite Count**: One sprite per visible block
+- **Light Updates**: Max 100 per frame (controlled by `LightingSystem._process`)
+
+### Memory Usage
+
+Check memory in Debugger → Monitor:
+- **Object Count**: Should stabilize after world generation
+- **Resource Count**: Textures cached, not recreated
+- **Memory**: Should not continuously increase
+
+## Debugging Scripts
+
+### Enable Verbose Logging
+
+Add to any script:
+
+```gdscript
+const DEBUG = true
+
+func _log(message: String):
+ if DEBUG:
+ print("[", get_script().get_path().get_file(), "] ", message)
+```
+
+### Visualize Chunk Boundaries
+
+Add to `IsometricRenderer`:
+
+```gdscript
+func _draw_chunk_boundaries():
+ for chunk_key in world_api.chunks.keys():
+ var chunk = world_api.chunks[chunk_key]
+ var min_pos = chunk.min_bound
+ var max_pos = chunk.max_bound
+ # Draw rectangles at chunk edges
+ # (implement using Line2D nodes)
+```
+
+### Inspect World State
+
+At any time in code:
+
+```gdscript
+func _print_world_stats():
+ print("=== World Stats ===")
+ print("Chunks loaded: ", world_api.chunks.size())
+ var total_blocks = 0
+ for chunk in world_api.chunks.values():
+ total_blocks += chunk.get_all_blocks().size()
+ print("Total blocks: ", total_blocks)
+ print("Block types: ", world_api.block_types.keys())
+ print("==================")
+```
+
+## Continuous Integration
+
+For automated testing in CI/CD:
+
+```bash
+# Example GitHub Actions workflow
+- name: Run Godot Tests
+ run: |
+ godot --headless --path . --script tests/run_tests.gd
+```
+
+## Getting Help
+
+If issues persist:
+
+1. Check console output for errors
+2. Verify all node paths in scenes match script expectations
+3. Ensure Godot version is 4.3+
+4. Review API.md for correct usage
+5. Create minimal reproduction scene
+6. Report issues with:
+ - Console output
+ - Steps to reproduce
+ - Expected vs actual behavior
+ - Godot version and OS
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..b370ceb
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 0000000..502cb4f
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://nn2cavvjr85b"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/project.godot b/project.godot
new file mode 100644
index 0000000..ab5c71e
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,108 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="OpenIsopix"
+config/description="An open project for 2D isometric pixel-art games"
+run/main_scene="res://scenes/Main.tscn"
+config/features=PackedStringArray("4.5", "Forward Plus")
+config/icon="res://icon.svg"
+
+[display]
+
+window/size/viewport_width=1280
+window/size/viewport_height=720
+window/stretch/mode="viewport"
+
+[input]
+
+ui_left={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+ui_right={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+ui_up={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+ui_down={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+zoom_in={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"canceled":false,"pressed":false,"double_click":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":61,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+zoom_out={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":45,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+rotate_left={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":81,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+rotate_right={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+change_pitch={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":80,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+place_block={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+remove_block={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
+]
+}
+toggle_fog={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":70,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+
+[layer_names]
+
+2d_render/layer_1="World"
+2d_render/layer_2="Fog"
+2d_render/layer_3="UI"
+
+[rendering]
+
+textures/canvas_textures/default_texture_filter=0
+anti_aliasing/quality/msaa_2d=1
diff --git a/scenes/Main.tscn b/scenes/Main.tscn
new file mode 100644
index 0000000..541dac6
--- /dev/null
+++ b/scenes/Main.tscn
@@ -0,0 +1,120 @@
+[gd_scene load_steps=8 format=3 uid="uid://b8wd8jvjfx4aw"]
+
+[ext_resource type="Script" path="res://scripts/Main.gd" id="1"]
+[ext_resource type="Script" path="res://scripts/core/WorldAPI.gd" id="2"]
+[ext_resource type="Script" path="res://scripts/rendering/IsometricCamera.gd" id="3"]
+[ext_resource type="Script" path="res://scripts/rendering/IsometricRenderer.gd" id="4"]
+[ext_resource type="Script" path="res://scripts/systems/LightingSystem.gd" id="5"]
+[ext_resource type="Script" path="res://scripts/systems/FogOfWarSystem.gd" id="6"]
+[ext_resource type="Script" path="res://scripts/systems/InteractionSystem.gd" id="7"]
+
+[node name="Main" type="Node2D"]
+script = ExtResource("1")
+
+[node name="WorldAPI" type="Node" parent="."]
+script = ExtResource("2")
+
+[node name="IsometricCamera" type="Camera2D" parent="."]
+script = ExtResource("3")
+
+[node name="IsometricRenderer" type="Node2D" parent="."]
+script = ExtResource("4")
+
+[node name="LightingSystem" type="Node" parent="."]
+script = ExtResource("5")
+
+[node name="FogOfWarSystem" type="Node" parent="."]
+script = ExtResource("6")
+
+[node name="InteractionSystem" type="Node" parent="."]
+script = ExtResource("7")
+
+[node name="UI" type="CanvasLayer" parent="."]
+
+[node name="HUD" type="Control" parent="UI"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+
+[node name="InfoPanel" type="PanelContainer" parent="UI/HUD"]
+layout_mode = 0
+offset_left = 10.0
+offset_top = 10.0
+offset_right = 350.0
+offset_bottom = 200.0
+
+[node name="VBox" type="VBoxContainer" parent="UI/HUD/InfoPanel"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="UI/HUD/InfoPanel/VBox"]
+layout_mode = 2
+text = "OpenIsopix Demo"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="UI/HUD/InfoPanel/VBox"]
+layout_mode = 2
+
+[node name="Controls" type="Label" parent="UI/HUD/InfoPanel/VBox"]
+layout_mode = 2
+text = "Arrow Keys: Move Camera
+P: Change Pitch
+Q/E: Rotate Camera
++/-: Zoom
+LMB: Place Block
+RMB: Remove Block
+1-5: Block Type
+F: Reveal Fog
+Space: Cycle Mode"
+
+[node name="HSeparator2" type="HSeparator" parent="UI/HUD/InfoPanel/VBox"]
+layout_mode = 2
+
+[node name="Status" type="Label" parent="UI/HUD/InfoPanel/VBox"]
+layout_mode = 2
+text = "Mode: SELECT
+Block: Grass"
+
+[node name="BottomPanel" type="PanelContainer" parent="UI/HUD"]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -200.0
+offset_top = -60.0
+offset_right = 200.0
+offset_bottom = -10.0
+grow_horizontal = 2
+grow_vertical = 0
+
+[node name="HBox" type="HBoxContainer" parent="UI/HUD/BottomPanel"]
+layout_mode = 2
+alignment = 1
+
+[node name="BlockButtons" type="HBoxContainer" parent="UI/HUD/BottomPanel/HBox"]
+layout_mode = 2
+
+[node name="GrassBtn" type="Button" parent="UI/HUD/BottomPanel/HBox/BlockButtons"]
+layout_mode = 2
+text = "1: Grass"
+
+[node name="StoneBtn" type="Button" parent="UI/HUD/BottomPanel/HBox/BlockButtons"]
+layout_mode = 2
+text = "2: Stone"
+
+[node name="WaterBtn" type="Button" parent="UI/HUD/BottomPanel/HBox/BlockButtons"]
+layout_mode = 2
+text = "3: Water"
+
+[node name="WoodBtn" type="Button" parent="UI/HUD/BottomPanel/HBox/BlockButtons"]
+layout_mode = 2
+text = "4: Wood"
+
+[node name="SoilBtn" type="Button" parent="UI/HUD/BottomPanel/HBox/BlockButtons"]
+layout_mode = 2
+text = "5: Soil"
\ No newline at end of file
diff --git a/scripts/Main.gd b/scripts/Main.gd
new file mode 100644
index 0000000..f1519b7
--- /dev/null
+++ b/scripts/Main.gd
@@ -0,0 +1,191 @@
+## OpenIsopix - Main Game Manager
+## Orchestrates all systems
+
+extends Node2D
+
+## System references
+@onready var world_api = $WorldAPI
+@onready var camera = $IsometricCamera
+@onready var renderer = $IsometricRenderer
+@onready var lighting_system = $LightingSystem
+@onready var fog_system = $FogOfWarSystem
+@onready var interaction_system = $InteractionSystem
+@onready var ui = $UI
+
+## Track camera world position for chunk loading
+var last_camera_world_pos: Vector3 = Vector3.ZERO
+
+func _ready():
+ print("OpenIsopix - Initializing...")
+
+ # Connect systems
+ _setup_systems()
+
+ # Generate demo world
+ _generate_demo_world()
+
+ # Initial render
+ if renderer:
+ renderer.render_world()
+
+ print("OpenIsopix - Ready!")
+ _print_controls()
+
+func _process(_delta):
+ # Update chunk loading based on camera movement
+ if world_api and camera:
+ # Convert camera screen position to approximate world position
+ # Camera position represents screen coordinates, we need to track actual world focus
+ var cam_world_pos = Vector3(camera.position.x / 100.0, 0, camera.position.y / 100.0)
+
+ # Only check chunk loading periodically (not every frame)
+ if cam_world_pos.distance_to(last_camera_world_pos) > 1.0:
+ last_camera_world_pos = cam_world_pos
+ var chunks_changed = world_api.update_chunk_loading(cam_world_pos)
+
+ # Only re-render if chunks actually changed
+ if chunks_changed and renderer:
+ print("Chunks changed - re-rendering world")
+ renderer.render_world()
+
+func _setup_systems():
+ if renderer:
+ renderer.world_api = world_api
+ renderer.camera = camera
+ renderer.initialize()
+
+ if lighting_system:
+ lighting_system.world_api = world_api
+
+ if fog_system:
+ fog_system.world_api = world_api
+
+ if interaction_system:
+ interaction_system.world_api = world_api
+ interaction_system.camera = camera
+ interaction_system.fog_system = fog_system
+ interaction_system.renderer = renderer
+ interaction_system.status_label = $UI/HUD/InfoPanel/VBox/Status
+ interaction_system.initialize()
+
+ var block_buttons = $UI/HUD/BottomPanel/HBox/BlockButtons
+ if block_buttons:
+ block_buttons.get_node("GrassBtn").pressed.connect(_on_grass_btn_pressed)
+ block_buttons.get_node("StoneBtn").pressed.connect(_on_stone_btn_pressed)
+ block_buttons.get_node("WaterBtn").pressed.connect(_on_water_btn_pressed)
+ block_buttons.get_node("WoodBtn").pressed.connect(_on_wood_btn_pressed)
+ block_buttons.get_node("SoilBtn").pressed.connect(_on_soil_btn_pressed)
+
+func _generate_demo_world():
+ if not world_api:
+ return
+
+ print("Generating demo world...")
+
+ # Create a simple test world with various block types
+ var world_size = 10
+
+ # Ground layer
+ for x in range(-world_size, world_size):
+ for z in range(-world_size, world_size):
+ # Create varied terrain
+ var block_type = "grass"
+
+ # Add some water
+ if (x + z) % 7 == 0:
+ block_type = "water"
+ # Add some stone
+ elif abs(x) > world_size / 2 or abs(z) > world_size / 2:
+ block_type = "stone"
+
+ world_api.add_block(Vector3i(x, 0, z), block_type)
+
+ # Add some elevated blocks
+ for i in range(5):
+ var x = randi() % (world_size * 2) - world_size
+ var z = randi() % (world_size * 2) - world_size
+ world_api.add_block(Vector3i(x, 1, z), "stone", 0.5)
+
+ # Reveal starting area
+ if fog_system:
+ fog_system.reveal_area(Vector3i(0, 0, 0), 12.0)
+
+ print("Demo world generated!")
+
+func _print_controls():
+ print("\n=== CONTROLS ===")
+ print("Arrow Keys / WASD: Move camera")
+ print("P: Cycle camera pitch")
+ print("+/-: Zoom in/out")
+ print("Mouse Wheel: Zoom in/out")
+ print("Left Click: Place block / Select")
+ print("Right Click: Remove block")
+ print("1-5: Select block type (press twice for full height)")
+ print("F: Reveal fog area")
+ print("G: Toggle fog globally")
+ print("Space: Cycle interaction mode")
+ print("Ctrl+Z: Undo")
+ print("Ctrl+Y: Redo")
+ print("================\n")
+
+func _input(event):
+ # Handle pitch change
+ if event.is_action_pressed("change_pitch"):
+ if camera:
+ camera.cycle_pitch()
+ if renderer:
+ renderer.render_world()
+
+ # Handle rotation
+ if event.is_action_pressed("rotate_left"):
+ print("Q pressed - rotating left")
+ if camera:
+ camera.rotate_heading(false) # Counter-clockwise
+ if renderer:
+ renderer.render_world()
+
+ if event.is_action_pressed("rotate_right"):
+ print("E pressed - rotating right")
+ if camera:
+ camera.rotate_heading(true) # Clockwise
+ if renderer:
+ renderer.render_world()
+
+ # Handle zoom
+ if event.is_action_pressed("zoom_in"):
+ if camera:
+ camera.zoom_camera(true)
+
+ if event.is_action_pressed("zoom_out"):
+ if camera:
+ camera.zoom_camera(false)
+
+func _on_grass_btn_pressed():
+ if interaction_system:
+ interaction_system.selected_block_type = "grass"
+ interaction_system._update_cursor_texture()
+ interaction_system._update_status_ui()
+
+func _on_stone_btn_pressed():
+ if interaction_system:
+ interaction_system.selected_block_type = "stone"
+ interaction_system._update_cursor_texture()
+ interaction_system._update_status_ui()
+
+func _on_water_btn_pressed():
+ if interaction_system:
+ interaction_system.selected_block_type = "water"
+ interaction_system._update_cursor_texture()
+ interaction_system._update_status_ui()
+
+func _on_wood_btn_pressed():
+ if interaction_system:
+ interaction_system.selected_block_type = "wood"
+ interaction_system._update_cursor_texture()
+ interaction_system._update_status_ui()
+
+func _on_soil_btn_pressed():
+ if interaction_system:
+ interaction_system.selected_block_type = "soil"
+ interaction_system._update_cursor_texture()
+ interaction_system._update_status_ui()
diff --git a/scripts/Main.gd.uid b/scripts/Main.gd.uid
new file mode 100644
index 0000000..d451b66
--- /dev/null
+++ b/scripts/Main.gd.uid
@@ -0,0 +1 @@
+uid://bwtrvs1s7tmq8
diff --git a/scripts/core/BlockInstance.gd b/scripts/core/BlockInstance.gd
new file mode 100644
index 0000000..6c798db
--- /dev/null
+++ b/scripts/core/BlockInstance.gd
@@ -0,0 +1,45 @@
+## OpenIsopix - Block Instance
+
+# Represents a single block instance in the world
+class_name BlockInstance
+extends RefCounted
+
+## Block position in world space (x, y, z)
+var position: Vector3i
+
+## Block type reference
+var block_type: BlockType
+
+## Current health points
+var hp: float
+
+## Custom properties (extensible for game-specific data)
+var custom_data: Dictionary = {}
+
+## Height level (0.5 or 1.0)
+var height: float = 1.0
+
+## Current lighting level (0.0 to 1.0)
+var light_level: float = 1.0
+
+## Whether this block has been revealed (for fog of war)
+var is_revealed: bool = false
+
+func _init(p_position: Vector3i, p_block_type: BlockType, p_height: float = 1.0):
+ position = p_position
+ block_type = p_block_type
+ height = p_height
+ hp = p_block_type.max_hp if p_block_type else 100.0
+
+func take_damage(amount: float) -> bool:
+ hp -= amount
+ return hp <= 0
+
+func heal(amount: float) -> void:
+ hp = min(hp + amount, block_type.max_hp if block_type else 100.0)
+
+func is_destroyed() -> bool:
+ return hp <= 0
+
+func get_type_id() -> String:
+ return block_type.id if block_type else ""
diff --git a/scripts/core/BlockInstance.gd.uid b/scripts/core/BlockInstance.gd.uid
new file mode 100644
index 0000000..17c2a91
--- /dev/null
+++ b/scripts/core/BlockInstance.gd.uid
@@ -0,0 +1 @@
+uid://0hxdy3k3ynur
diff --git a/scripts/core/BlockType.gd b/scripts/core/BlockType.gd
new file mode 100644
index 0000000..a87534f
--- /dev/null
+++ b/scripts/core/BlockType.gd
@@ -0,0 +1,52 @@
+## OpenIsopix - Core Block Types
+
+# Represents a single block type definition
+class_name BlockType
+extends Resource
+
+## Block type identifier (e.g., "grass", "stone", "water")
+@export var id: String = ""
+
+## Display name
+@export var display_name: String = ""
+
+## Behavior flags
+@export var is_solid: bool = true
+@export var is_opaque: bool = true
+@export var is_climbable: bool = false
+@export var is_walkable: bool = true
+
+## Visual attributes
+@export var texture: Texture2D = null
+@export var animated: bool = false
+@export var animation_frames: Array[Texture2D] = []
+@export var animation_speed: float = 1.0
+@export var emits_light: bool = false
+@export var light_color: Color = Color.WHITE
+@export var light_intensity: float = 1.0
+@export var light_radius: float = 100.0
+
+## Numeric attributes
+@export var max_hp: float = 100.0
+@export var material_type: String = "generic"
+@export var hardness: float = 1.0
+@export var height: float = 1.0 # Block height: 0.5 or 1.0
+
+## Interaction callbacks (will be handled by signals)
+@export var on_click_enabled: bool = true
+@export var on_walk_over_enabled: bool = false
+
+func _init(
+ p_id: String = "",
+ p_display_name: String = "",
+ p_texture: Texture2D = null
+):
+ id = p_id
+ display_name = p_display_name
+ texture = p_texture
+
+func get_current_frame(time: float) -> Texture2D:
+ if not animated or animation_frames.is_empty():
+ return texture
+ var frame_index = int(time * animation_speed) % animation_frames.size()
+ return animation_frames[frame_index]
diff --git a/scripts/core/BlockType.gd.uid b/scripts/core/BlockType.gd.uid
new file mode 100644
index 0000000..e80566c
--- /dev/null
+++ b/scripts/core/BlockType.gd.uid
@@ -0,0 +1 @@
+uid://7h4sromk1lsu
diff --git a/scripts/core/Chunk.gd b/scripts/core/Chunk.gd
new file mode 100644
index 0000000..cc31069
--- /dev/null
+++ b/scripts/core/Chunk.gd
@@ -0,0 +1,82 @@
+## OpenIsopix - World Chunk
+
+# Represents a 16x16 chunk of blocks
+class_name Chunk
+extends RefCounted
+
+const CHUNK_SIZE = 16
+
+## Chunk position in chunk coordinates
+var chunk_pos: Vector2i
+
+## 3D array of block instances [x][y][z]
+## y represents height layers (we support multiple height levels)
+var blocks: Array = []
+
+## Whether this chunk is currently loaded
+var is_loaded: bool = false
+
+## Chunk bounds (for optimization)
+var min_bound: Vector3i
+var max_bound: Vector3i
+
+func _init(p_chunk_pos: Vector2i):
+ chunk_pos = p_chunk_pos
+ min_bound = Vector3i(chunk_pos.x * CHUNK_SIZE, 0, chunk_pos.y * CHUNK_SIZE)
+ max_bound = Vector3i((chunk_pos.x + 1) * CHUNK_SIZE, 10, (chunk_pos.y + 1) * CHUNK_SIZE)
+ _initialize_blocks()
+
+func _initialize_blocks():
+ blocks = []
+ for x in range(CHUNK_SIZE):
+ blocks.append([])
+ for z in range(CHUNK_SIZE):
+ blocks[x].append([])
+
+func set_block(local_pos: Vector3i, block: BlockInstance) -> void:
+ if not _is_valid_local_pos(local_pos):
+ return
+
+ # Ensure height array exists
+ while blocks[local_pos.x][local_pos.z].size() <= local_pos.y:
+ blocks[local_pos.x][local_pos.z].append(null)
+
+ blocks[local_pos.x][local_pos.z][local_pos.y] = block
+
+func get_block(local_pos: Vector3i) -> BlockInstance:
+ if not _is_valid_local_pos(local_pos):
+ return null
+
+ if local_pos.y >= blocks[local_pos.x][local_pos.z].size():
+ return null
+
+ return blocks[local_pos.x][local_pos.z][local_pos.y]
+
+func remove_block(local_pos: Vector3i) -> void:
+ if not _is_valid_local_pos(local_pos):
+ return
+
+ if local_pos.y < blocks[local_pos.x][local_pos.z].size():
+ blocks[local_pos.x][local_pos.z][local_pos.y] = null
+
+func _is_valid_local_pos(local_pos: Vector3i) -> bool:
+ return (local_pos.x >= 0 and local_pos.x < CHUNK_SIZE and
+ local_pos.z >= 0 and local_pos.z < CHUNK_SIZE and
+ local_pos.y >= 0)
+
+func to_world_pos(local_pos: Vector3i) -> Vector3i:
+ return Vector3i(
+ chunk_pos.x * CHUNK_SIZE + local_pos.x,
+ local_pos.y,
+ chunk_pos.y * CHUNK_SIZE + local_pos.z
+ )
+
+func get_all_blocks() -> Array[BlockInstance]:
+ var result: Array[BlockInstance] = []
+ for x in range(CHUNK_SIZE):
+ for z in range(CHUNK_SIZE):
+ for y in range(blocks[x][z].size()):
+ var block = blocks[x][z][y]
+ if block != null:
+ result.append(block)
+ return result
diff --git a/scripts/core/Chunk.gd.uid b/scripts/core/Chunk.gd.uid
new file mode 100644
index 0000000..98d131e
--- /dev/null
+++ b/scripts/core/Chunk.gd.uid
@@ -0,0 +1 @@
+uid://xxm3up8xxu6s
diff --git a/scripts/core/WorldAPI.gd b/scripts/core/WorldAPI.gd
new file mode 100644
index 0000000..350d479
--- /dev/null
+++ b/scripts/core/WorldAPI.gd
@@ -0,0 +1,256 @@
+## OpenIsopix - World Manager API
+## Core API for world interaction - decoupled from UI
+
+class_name WorldAPI
+extends Node
+
+## Signals for world events
+signal block_added(position: Vector3i, block: BlockInstance)
+signal block_removed(position: Vector3i)
+signal block_modified(position: Vector3i, block: BlockInstance)
+signal block_clicked(position: Vector3i, block: BlockInstance)
+signal block_walked_over(position: Vector3i, block: BlockInstance)
+signal chunk_loaded(chunk_pos: Vector2i)
+signal chunk_unloaded(chunk_pos: Vector2i)
+signal lighting_changed(position: Vector3i, light_level: float)
+
+const CHUNK_SIZE = 16
+
+## All loaded chunks [chunk_pos_key] = Chunk
+var chunks: Dictionary = {}
+
+## Block type registry [type_id] = BlockType
+var block_types: Dictionary = {}
+
+## Chunk load distance (in chunks)
+@export var chunk_load_distance: int = 3
+
+## Current camera position for chunk loading
+var camera_chunk_pos: Vector2i = Vector2i.ZERO
+
+## Loaded chunk positions
+var loaded_chunk_positions: Array[Vector2i] = []
+
+func _ready():
+ _register_default_block_types()
+
+## Register a new block type
+func register_block_type(block_type: BlockType) -> void:
+ if block_type.id.is_empty():
+ push_error("Cannot register block type with empty ID")
+ return
+ block_types[block_type.id] = block_type
+
+## Get a registered block type
+func get_block_type(type_id: String) -> BlockType:
+ return block_types.get(type_id)
+
+## Add a block to the world
+func add_block(world_pos: Vector3i, type_id: String, height: float = 1.0) -> BlockInstance:
+ var block_type = get_block_type(type_id)
+ if not block_type:
+ push_error("Unknown block type: " + type_id)
+ return null
+
+ var chunk_pos = _world_to_chunk_pos(world_pos)
+ var chunk = _get_or_create_chunk(chunk_pos)
+ var local_pos = _world_to_local_pos(world_pos)
+
+ var block = BlockInstance.new(world_pos, block_type, height)
+ chunk.set_block(local_pos, block)
+
+ block_added.emit(world_pos, block)
+ return block
+
+## Remove a block from the world
+func remove_block(world_pos: Vector3i) -> void:
+ var chunk_pos = _world_to_chunk_pos(world_pos)
+ var chunk = _get_chunk(chunk_pos)
+ if not chunk:
+ return
+
+ var local_pos = _world_to_local_pos(world_pos)
+ chunk.remove_block(local_pos)
+ block_removed.emit(world_pos)
+
+## Get a block at world position
+func get_block(world_pos: Vector3i) -> BlockInstance:
+ var chunk_pos = _world_to_chunk_pos(world_pos)
+ var chunk = _get_chunk(chunk_pos)
+ if not chunk:
+ return null
+
+ var local_pos = _world_to_local_pos(world_pos)
+ return chunk.get_block(local_pos)
+
+## Modify a block's properties
+func modify_block(world_pos: Vector3i, property: String, value: Variant) -> void:
+ var block = get_block(world_pos)
+ if not block:
+ return
+
+ match property:
+ "hp":
+ block.hp = value
+ "light_level":
+ block.light_level = value
+ lighting_changed.emit(world_pos, value)
+ "is_revealed":
+ block.is_revealed = value
+ _:
+ block.custom_data[property] = value
+
+ block_modified.emit(world_pos, block)
+
+## Query block attributes
+func get_block_attribute(world_pos: Vector3i, attribute: String) -> Variant:
+ var block = get_block(world_pos)
+ if not block:
+ return null
+
+ match attribute:
+ "position": return block.position
+ "type_id": return block.get_type_id()
+ "hp": return block.hp
+ "height": return block.height
+ "light_level": return block.light_level
+ "is_revealed": return block.is_revealed
+ "is_solid": return block.block_type.is_solid if block.block_type else false
+ "is_opaque": return block.block_type.is_opaque if block.block_type else false
+ "is_climbable": return block.block_type.is_climbable if block.block_type else false
+ _: return block.custom_data.get(attribute)
+
+## Load a chunk
+func load_chunk(chunk_pos: Vector2i) -> Chunk:
+ var chunk = _get_or_create_chunk(chunk_pos)
+ if not chunk.is_loaded:
+ chunk.is_loaded = true
+ chunk_loaded.emit(chunk_pos)
+ return chunk
+
+## Unload a chunk
+func unload_chunk(chunk_pos: Vector2i) -> void:
+ var key = _chunk_pos_to_key(chunk_pos)
+ if chunks.has(key):
+ chunks[key].is_loaded = false
+ chunks.erase(key)
+ chunk_unloaded.emit(chunk_pos)
+
+## Get all blocks in a chunk
+func get_chunk_blocks(chunk_pos: Vector2i) -> Array[BlockInstance]:
+ var chunk = _get_chunk(chunk_pos)
+ if not chunk:
+ return []
+ return chunk.get_all_blocks()
+
+## Trigger block click event
+func trigger_block_click(world_pos: Vector3i) -> void:
+ var block = get_block(world_pos)
+ if block and block.block_type and block.block_type.on_click_enabled:
+ block_clicked.emit(world_pos, block)
+
+## Trigger block walk over event
+func trigger_block_walk_over(world_pos: Vector3i) -> void:
+ var block = get_block(world_pos)
+ if block and block.block_type and block.block_type.on_walk_over_enabled:
+ block_walked_over.emit(world_pos, block)
+
+## Helper functions
+func _world_to_chunk_pos(world_pos: Vector3i) -> Vector2i:
+ return Vector2i(
+ floori(float(world_pos.x) / CHUNK_SIZE),
+ floori(float(world_pos.z) / CHUNK_SIZE)
+ )
+
+func _world_to_local_pos(world_pos: Vector3i) -> Vector3i:
+ return Vector3i(
+ posmod(world_pos.x, CHUNK_SIZE),
+ world_pos.y,
+ posmod(world_pos.z, CHUNK_SIZE)
+ )
+
+func _chunk_pos_to_key(chunk_pos: Vector2i) -> String:
+ return str(chunk_pos.x) + "," + str(chunk_pos.y)
+
+func _get_chunk(chunk_pos: Vector2i) -> Chunk:
+ var key = _chunk_pos_to_key(chunk_pos)
+ return chunks.get(key)
+
+func _get_or_create_chunk(chunk_pos: Vector2i) -> Chunk:
+ var key = _chunk_pos_to_key(chunk_pos)
+ if not chunks.has(key):
+ var chunk = Chunk.new(chunk_pos)
+ chunks[key] = chunk
+ return chunks[key]
+
+## Update chunk loading based on camera position
+func update_chunk_loading(camera_world_pos: Vector3) -> bool:
+ var new_camera_chunk = Vector2i(
+ floori(camera_world_pos.x / CHUNK_SIZE),
+ floori(camera_world_pos.z / CHUNK_SIZE)
+ )
+
+ # Only update if camera moved to a different chunk
+ if new_camera_chunk == camera_chunk_pos:
+ return false
+
+ camera_chunk_pos = new_camera_chunk
+
+ # Calculate which chunks should be loaded
+ var chunks_to_load: Array[Vector2i] = []
+ for x in range(-chunk_load_distance, chunk_load_distance + 1):
+ for z in range(-chunk_load_distance, chunk_load_distance + 1):
+ var chunk_pos = Vector2i(camera_chunk_pos.x + x, camera_chunk_pos.y + z)
+ chunks_to_load.append(chunk_pos)
+
+ var chunks_changed = false
+
+ # Unload chunks that are too far
+ for loaded_pos in loaded_chunk_positions.duplicate():
+ if not chunks_to_load.has(loaded_pos):
+ unload_chunk(loaded_pos)
+ loaded_chunk_positions.erase(loaded_pos)
+ chunks_changed = true
+
+ # Load new chunks
+ for chunk_pos in chunks_to_load:
+ if not loaded_chunk_positions.has(chunk_pos):
+ load_chunk(chunk_pos)
+ loaded_chunk_positions.append(chunk_pos)
+ chunks_changed = true
+
+ return chunks_changed
+
+func _register_default_block_types():
+ # Register some default block types
+ var grass = BlockType.new("grass", "Grass Block")
+ grass.is_solid = true
+ grass.is_opaque = true
+ grass.height = 0.5 # Half block
+ register_block_type(grass)
+
+ var stone = BlockType.new("stone", "Stone Block")
+ stone.is_solid = true
+ stone.is_opaque = true
+ stone.hardness = 2.0
+ stone.height = 0.5 # Half block (can be toggled to 1.0)
+ register_block_type(stone)
+
+ var water = BlockType.new("water", "Water")
+ water.is_solid = false
+ water.is_opaque = false
+ water.is_walkable = false
+ water.height = 0.5 # Half block
+ register_block_type(water)
+
+ var wood = BlockType.new("wood", "Wood")
+ wood.is_solid = true
+ wood.is_opaque = true
+ wood.height = 0.5 # Half block (can be toggled to 1.0)
+ register_block_type(wood)
+
+ var soil = BlockType.new("soil", "Soil")
+ soil.is_solid = true
+ soil.is_opaque = true
+ soil.height = 0.5 # Half block
+ register_block_type(soil)
diff --git a/scripts/core/WorldAPI.gd.uid b/scripts/core/WorldAPI.gd.uid
new file mode 100644
index 0000000..3b4d213
--- /dev/null
+++ b/scripts/core/WorldAPI.gd.uid
@@ -0,0 +1 @@
+uid://j8j6ler26sos
diff --git a/scripts/rendering/IsometricCamera.gd b/scripts/rendering/IsometricCamera.gd
new file mode 100644
index 0000000..4f0e16e
--- /dev/null
+++ b/scripts/rendering/IsometricCamera.gd
@@ -0,0 +1,182 @@
+## OpenIsopix - Camera Controller
+## Manages POV (Point of View) with rotation, zoom, and pitch
+
+class_name IsometricCamera
+extends Camera2D
+
+## POV settings
+enum Heading { NORTH, EAST, SOUTH, WEST }
+enum PitchLevel { LOW, NORMAL, HIGH, TOP_DOWN }
+
+## Current POV state
+var current_heading: Heading = Heading.NORTH
+var current_pitch: PitchLevel = PitchLevel.NORMAL
+var current_zoom_level: float = 1.0
+
+## Zoom settings
+@export var min_zoom: float = 0.5
+@export var max_zoom: float = 3.0
+@export var zoom_speed: float = 0.1
+
+## Pitch angles (in degrees for visual reference)
+const PITCH_ANGLES = {
+ PitchLevel.LOW: 30.0, # Closer to ground
+ PitchLevel.NORMAL: 45.0, # Standard isometric
+ PitchLevel.HIGH: 60.0, # Closer to top-down
+ PitchLevel.TOP_DOWN: 90.0 # Pure top-down
+}
+
+## Movement settings
+@export var pan_speed: float = 500.0
+@export var smooth_movement: bool = true
+@export var smoothing_factor: float = 5.0
+
+const TILE_HALF_WIDTH: float = 24.0
+const TILE_HALF_HEIGHT: float = 12.0
+const BLOCK_TOP_FACE_HEIGHT: float = 24.0
+
+var target_position: Vector2
+var velocity: Vector2 = Vector2.ZERO
+
+## Signals
+signal heading_changed(new_heading: Heading)
+signal pitch_changed(new_pitch: PitchLevel)
+signal zoom_changed(new_zoom: float)
+
+func _ready():
+ target_position = position
+ zoom = Vector2(current_zoom_level, current_zoom_level)
+
+func _process(delta):
+ if smooth_movement:
+ position = position.lerp(target_position, smoothing_factor * delta)
+ else:
+ position = target_position
+
+ # Handle camera movement
+ _handle_movement(delta)
+
+func _handle_movement(delta: float):
+ var move_input = Vector2.ZERO
+
+ # Get input direction
+ if Input.is_action_pressed("ui_left"):
+ move_input.x -= 1
+ if Input.is_action_pressed("ui_right"):
+ move_input.x += 1
+ if Input.is_action_pressed("ui_up"):
+ move_input.y -= 1
+ if Input.is_action_pressed("ui_down"):
+ move_input.y += 1
+
+ # Apply rotation to movement based on heading
+ var rotated_input = _rotate_input_by_heading(move_input)
+
+ # Update target position
+ if rotated_input.length() > 0:
+ target_position += rotated_input.normalized() * pan_speed * delta / current_zoom_level
+
+func _rotate_input_by_heading(input: Vector2) -> Vector2:
+ match current_heading:
+ Heading.NORTH:
+ return input
+ Heading.EAST:
+ return Vector2(-input.y, input.x)
+ Heading.SOUTH:
+ return Vector2(-input.x, -input.y)
+ Heading.WEST:
+ return Vector2(input.y, -input.x)
+ return input
+
+## Rotate camera heading
+func rotate_heading(clockwise: bool = true):
+ if clockwise:
+ current_heading = (current_heading + 1) % 4 as Heading
+ else:
+ current_heading = (current_heading - 1 + 4) % 4 as Heading
+ heading_changed.emit(current_heading)
+
+## Change pitch level
+func cycle_pitch():
+ current_pitch = (current_pitch + 1) % 4 as PitchLevel
+ pitch_changed.emit(current_pitch)
+
+## Zoom in/out
+func zoom_camera(zoom_in: bool):
+ var zoom_delta = zoom_speed if zoom_in else -zoom_speed
+ current_zoom_level = clamp(current_zoom_level + zoom_delta, min_zoom, max_zoom)
+ zoom = Vector2(current_zoom_level, current_zoom_level)
+ zoom_changed.emit(current_zoom_level)
+
+## Set camera position
+func set_camera_position(new_position: Vector2):
+ target_position = new_position
+ if not smooth_movement:
+ position = new_position
+
+## Get current heading as rotation angle
+func get_heading_rotation() -> float:
+ return current_heading * 90.0
+
+## Get current pitch angle
+func get_pitch_angle() -> float:
+ return PITCH_ANGLES[current_pitch]
+
+## Convert world position to isometric screen position
+func world_to_iso(world_pos: Vector3) -> Vector2:
+ var rotated_pos = _rotate_world_pos_by_heading(world_pos)
+ var pitch_factor = get_pitch_angle() / 45.0
+
+ # Isometric projection: x-z plane to diamond
+ var iso_x = (rotated_pos.x - rotated_pos.z) * TILE_HALF_WIDTH
+ var iso_y_xz = (rotated_pos.x + rotated_pos.z) * TILE_HALF_HEIGHT * pitch_factor
+ var iso_y_height = rotated_pos.y * BLOCK_TOP_FACE_HEIGHT
+
+ return Vector2(iso_x, iso_y_xz - iso_y_height)
+
+## Convert screen position to world position (approximate)
+func screen_to_world(screen_pos: Vector2) -> Vector3:
+ var cam_pos = get_screen_center_position()
+ var relative_pos = (screen_pos - cam_pos)
+
+ var pitch_factor = get_pitch_angle() / 45.0
+ if pitch_factor == 0: pitch_factor = 0.001
+
+ # Inverse isometric transform
+ var denom_x = TILE_HALF_WIDTH * 2.0
+ var denom_y_xz = TILE_HALF_HEIGHT * 2.0 * pitch_factor
+
+ var world_x = (relative_pos.x / denom_x) + (relative_pos.y / denom_y_xz)
+ var world_z = (relative_pos.y / denom_y_xz) - (relative_pos.x / denom_x)
+
+ # Undo camera rotation to get actual world coords
+ var rotated_result = Vector3(world_x, 0, world_z)
+ var actual_world_pos = _unrotate_world_pos_by_heading(rotated_result)
+
+ return actual_world_pos
+
+## Rotate world position based on current heading
+func _rotate_world_pos_by_heading(pos: Vector3) -> Vector3:
+ match current_heading:
+ Heading.NORTH:
+ return pos # No rotation
+ Heading.EAST:
+ return Vector3(pos.z, pos.y, -pos.x) # 90° clockwise
+ Heading.SOUTH:
+ return Vector3(-pos.x, pos.y, -pos.z) # 180°
+ Heading.WEST:
+ return Vector3(-pos.z, pos.y, pos.x) # 270° clockwise
+ return pos
+
+## Reverse rotation to get actual world coordinates
+func _unrotate_world_pos_by_heading(pos: Vector3) -> Vector3:
+ match current_heading:
+ Heading.NORTH:
+ return pos # No rotation
+ Heading.EAST:
+ return Vector3(-pos.z, pos.y, pos.x) # Reverse 90° clockwise
+ Heading.SOUTH:
+ return Vector3(-pos.x, pos.y, -pos.z) # Reverse 180°
+ Heading.WEST:
+ return Vector3(pos.z, pos.y, -pos.x) # Reverse 270° clockwise
+ return pos
\ No newline at end of file
diff --git a/scripts/rendering/IsometricCamera.gd.uid b/scripts/rendering/IsometricCamera.gd.uid
new file mode 100644
index 0000000..eb02a0d
--- /dev/null
+++ b/scripts/rendering/IsometricCamera.gd.uid
@@ -0,0 +1 @@
+uid://dbrr7kmaodqs5
diff --git a/scripts/rendering/IsometricRenderer.gd b/scripts/rendering/IsometricRenderer.gd
new file mode 100644
index 0000000..ea8bdbc
--- /dev/null
+++ b/scripts/rendering/IsometricRenderer.gd
@@ -0,0 +1,172 @@
+## OpenIsopix - Isometric Renderer
+## Renders the block-based world in isometric view
+
+class_name IsometricRenderer
+extends Node2D
+
+@export var world_api: WorldAPI
+@export var camera: IsometricCamera
+
+## Tile size in pixels (increased for isometric diamond tiles)
+const TILE_SIZE = 64
+const HALF_TILE = TILE_SIZE / 2
+
+## Rendering layers
+var world_layer: Node2D
+var fog_layer: CanvasLayer
+
+## Cache for rendered sprites
+var block_sprites: Dictionary = {}
+var textures: Dictionary = {}
+
+func _ready():
+ pass
+
+func initialize():
+ _setup_layers()
+ _generate_placeholder_textures()
+
+ if world_api:
+ world_api.block_added.connect(_on_block_added)
+ world_api.block_removed.connect(_on_block_removed)
+ world_api.block_modified.connect(_on_block_modified)
+ world_api.chunk_loaded.connect(_on_chunk_loaded)
+
+ if camera:
+ camera.heading_changed.connect(_on_heading_changed)
+ camera.pitch_changed.connect(_on_pitch_changed)
+
+func _setup_layers():
+ world_layer = Node2D.new()
+ add_child(world_layer)
+
+ fog_layer = CanvasLayer.new()
+ fog_layer.layer = 2
+ add_child(fog_layer)
+
+func _generate_placeholder_textures():
+ # Load isometric SVG textures for block types
+ textures["grass"] = load("res://assets/blocks/isometric-grass.svg")
+ textures["stone"] = load("res://assets/blocks/isometric-stone.svg")
+ textures["water"] = load("res://assets/blocks/isometric-water.svg")
+ textures["wood"] = load("res://assets/blocks/isometric-wood.svg")
+ textures["soil"] = load("res://assets/blocks/isometric-soil.svg")
+
+ print("Loaded isometric block textures: ", textures.keys())
+
+func _create_colored_texture(color: Color) -> ImageTexture:
+ var image = Image.create(TILE_SIZE, TILE_SIZE, false, Image.FORMAT_RGBA8)
+ image.fill(color)
+
+ # Add some shading for 3D effect
+ for y in range(TILE_SIZE):
+ for x in range(TILE_SIZE):
+ if x < 4 or y < 4: # Top and left edges lighter
+ var current = image.get_pixel(x, y)
+ image.set_pixel(x, y, current.lightened(0.2))
+ elif x >= TILE_SIZE - 4 or y >= TILE_SIZE - 4: # Bottom and right edges darker
+ var current = image.get_pixel(x, y)
+ image.set_pixel(x, y, current.darkened(0.2))
+
+ return ImageTexture.create_from_image(image)
+
+func render_world():
+ # Clear existing sprites
+ _clear_all_sprites()
+
+ if not world_api:
+ return
+
+ # Get all chunks and render blocks
+ for chunk_key in world_api.chunks.keys():
+ var chunk = world_api.chunks[chunk_key]
+ if chunk.is_loaded:
+ _render_chunk(chunk)
+
+func _render_chunk(chunk: Chunk):
+ var blocks = chunk.get_all_blocks()
+ # Sort for painter's algorithm (back to front)
+ blocks.sort_custom(_sort_blocks_for_rendering)
+
+ for block in blocks:
+ _render_block(block)
+
+func _render_block(block: BlockInstance):
+ if not block or not block.block_type:
+ return
+
+ var world_pos = block.position
+ var key = _world_pos_to_key(world_pos)
+
+ # Create sprite if it doesn't exist
+ if not block_sprites.has(key):
+ var sprite = Sprite2D.new()
+ world_layer.add_child(sprite)
+ block_sprites[key] = sprite
+
+ var sprite = block_sprites[key]
+
+ # Set texture
+ var texture = textures.get(block.block_type.id)
+ if texture:
+ sprite.texture = texture
+ else:
+ print("WARNING: No texture for block type: ", block.block_type.id)
+
+ var iso_pos = camera.world_to_iso(Vector3(world_pos.x, world_pos.y, world_pos.z))
+ sprite.position = iso_pos
+ var height_scale = block.height
+ sprite.scale = Vector2(1, height_scale)
+
+ # Apply lighting
+ sprite.modulate = Color(block.light_level, block.light_level, block.light_level, 1.0)
+
+ # Apply fog of war
+ if not block.is_revealed:
+ sprite.modulate.a = 0.3
+
+ # Z-index for proper layering
+ sprite.z_index = world_pos.x + world_pos.z + world_pos.y * 1000
+
+func _clear_all_sprites():
+ for key in block_sprites.keys():
+ var sprite = block_sprites[key]
+ if sprite:
+ sprite.queue_free()
+ block_sprites.clear()
+
+func _remove_sprite(world_pos: Vector3i):
+ var key = _world_pos_to_key(world_pos)
+ if block_sprites.has(key):
+ var sprite = block_sprites[key]
+ sprite.queue_free()
+ block_sprites.erase(key)
+
+func _world_pos_to_key(pos: Vector3i) -> String:
+ return str(pos.x) + "," + str(pos.y) + "," + str(pos.z)
+
+func _sort_blocks_for_rendering(a: BlockInstance, b: BlockInstance) -> bool:
+ var depth_a = a.position.x + a.position.z + a.position.y * 1000
+ var depth_b = b.position.x + b.position.z + b.position.y * 1000
+ return depth_a < depth_b
+
+## Signal handlers
+func _on_block_added(position: Vector3i, block: BlockInstance):
+ _render_block(block)
+
+func _on_block_removed(position: Vector3i):
+ _remove_sprite(position)
+
+func _on_block_modified(position: Vector3i, block: BlockInstance):
+ _render_block(block)
+
+func _on_chunk_loaded(chunk_pos: Vector2i):
+ render_world()
+
+func _on_heading_changed(new_heading):
+ # Re-render world with new rotation
+ render_world()
+
+func _on_pitch_changed(new_pitch):
+ # Re-render world with new pitch
+ render_world()
diff --git a/scripts/rendering/IsometricRenderer.gd.uid b/scripts/rendering/IsometricRenderer.gd.uid
new file mode 100644
index 0000000..231612f
--- /dev/null
+++ b/scripts/rendering/IsometricRenderer.gd.uid
@@ -0,0 +1 @@
+uid://bx2jhvbbsu6lx
diff --git a/scripts/systems/FogOfWarSystem.gd b/scripts/systems/FogOfWarSystem.gd
new file mode 100644
index 0000000..c35fa1d
--- /dev/null
+++ b/scripts/systems/FogOfWarSystem.gd
@@ -0,0 +1,128 @@
+## OpenIsopix - Fog of War System
+## Manages map revelation and exploration
+
+class_name FogOfWarSystem
+extends Node
+
+@export var world_api: WorldAPI
+
+## Revelation settings
+@export var revelation_radius: float = 8.0
+@export var auto_reveal: bool = false
+
+## Global fog state
+var is_fog_disabled: bool = false
+
+## Revealed positions cache
+var revealed_positions: Dictionary = {} # [position_key] = true
+
+signal area_revealed(center: Vector3i, radius: float)
+signal block_revealed(position: Vector3i)
+
+func _ready():
+ pass
+
+## Reveal an area around a position
+func reveal_area(center: Vector3i, radius: float = -1.0):
+ if radius < 0:
+ radius = revelation_radius
+
+ var radius_squared = radius * radius
+ var int_radius = int(ceil(radius))
+
+ for x in range(-int_radius, int_radius + 1):
+ for z in range(-int_radius, int_radius + 1):
+ for y in range(-2, 3): # Check a few height levels
+ var offset = Vector3i(x, y, z)
+ var check_pos = center + offset
+
+ # Check if within radius
+ var distance_squared = offset.x * offset.x + offset.z * offset.z
+ if distance_squared <= radius_squared:
+ _reveal_block(check_pos)
+
+ area_revealed.emit(center, radius)
+
+## Reveal a single block
+func _reveal_block(world_pos: Vector3i):
+ var key = _pos_to_key(world_pos)
+
+ if revealed_positions.has(key):
+ return # Already revealed
+
+ revealed_positions[key] = true
+
+ if world_api:
+ var block = world_api.get_block(world_pos)
+ if block and not block.is_revealed:
+ world_api.modify_block(world_pos, "is_revealed", true)
+ block_revealed.emit(world_pos)
+
+## Check if a position is revealed
+func is_revealed(world_pos: Vector3i) -> bool:
+ var key = _pos_to_key(world_pos)
+ return revealed_positions.has(key)
+
+## Hide an area (for dynamic fog)
+func hide_area(center: Vector3i, radius: float = -1.0):
+ if radius < 0:
+ radius = revelation_radius
+
+ var radius_squared = radius * radius
+ var int_radius = int(ceil(radius))
+
+ for x in range(-int_radius, int_radius + 1):
+ for z in range(-int_radius, int_radius + 1):
+ for y in range(-2, 3):
+ var offset = Vector3i(x, y, z)
+ var check_pos = center + offset
+
+ var distance_squared = offset.x * offset.x + offset.z * offset.z
+ if distance_squared <= radius_squared:
+ _hide_block(check_pos)
+
+func _hide_block(world_pos: Vector3i):
+ var key = _pos_to_key(world_pos)
+ revealed_positions.erase(key)
+
+ if world_api:
+ world_api.modify_block(world_pos, "is_revealed", false)
+
+## Reveal entire map (cheat/debug mode)
+func reveal_all():
+ if not world_api:
+ return
+
+ for chunk_key in world_api.chunks.keys():
+ var chunk = world_api.chunks[chunk_key]
+ if chunk.is_loaded:
+ var blocks = chunk.get_all_blocks()
+ for block in blocks:
+ if block:
+ _reveal_block(block.position)
+
+## Hide entire map
+func hide_all():
+ revealed_positions.clear()
+
+ if world_api:
+ for chunk_key in world_api.chunks.keys():
+ var chunk = world_api.chunks[chunk_key]
+ if chunk.is_loaded:
+ var blocks = chunk.get_all_blocks()
+ for block in blocks:
+ if block:
+ world_api.modify_block(block.position, "is_revealed", false)
+
+## Toggle fog globally (reveal all or hide all)
+func toggle_fog_globally():
+ is_fog_disabled = not is_fog_disabled
+ if is_fog_disabled:
+ reveal_all()
+ print("Fog disabled - all blocks revealed")
+ else:
+ hide_all()
+ print("Fog enabled - all blocks hidden")
+
+func _pos_to_key(pos: Vector3i) -> String:
+ return str(pos.x) + "," + str(pos.y) + "," + str(pos.z)
diff --git a/scripts/systems/FogOfWarSystem.gd.uid b/scripts/systems/FogOfWarSystem.gd.uid
new file mode 100644
index 0000000..a7b181c
--- /dev/null
+++ b/scripts/systems/FogOfWarSystem.gd.uid
@@ -0,0 +1 @@
+uid://c5s2ublj2ylh2
diff --git a/scripts/systems/InteractionSystem.gd b/scripts/systems/InteractionSystem.gd
new file mode 100644
index 0000000..49ff89b
--- /dev/null
+++ b/scripts/systems/InteractionSystem.gd
@@ -0,0 +1,351 @@
+## OpenIsopix - Interaction System
+## Handles user input and world interaction
+
+class_name InteractionSystem
+extends Node
+
+@export var world_api: WorldAPI
+@export var camera: IsometricCamera
+@export var fog_system: FogOfWarSystem
+@export var renderer: Node2D
+
+## UI References
+var status_label: Label
+
+## Current interaction mode
+enum InteractionMode { SELECT, PLACE, REMOVE }
+var current_mode: InteractionMode = InteractionMode.SELECT
+
+## Currently selected block type for placement
+var selected_block_type: String = "grass"
+var selected_block_height_override: float = -1.0 # -1 means use default from block type
+var last_selected_block: String = "" # Track for double-press detection
+
+## Undo/Redo history
+var action_history: Array[Dictionary] = []
+var history_index: int = -1
+const MAX_HISTORY_SIZE: int = 50
+
+## Currently hovered/selected block
+var hovered_position: Vector3i = Vector3i.ZERO
+var selected_position: Vector3i = Vector3i.ZERO
+var is_position_valid: bool = false
+
+## Highlight sprite for cursor
+var highlight_sprite: Sprite2D
+
+## Mouse/controller state
+var mouse_position: Vector2 = Vector2.ZERO
+
+signal block_selected(position: Vector3i)
+signal block_hovered(position: Vector3i)
+signal interaction_mode_changed(mode: InteractionMode)
+
+func _ready():
+ pass
+
+func initialize():
+ _create_highlight_sprite()
+ _update_cursor_texture()
+ _update_status_ui()
+
+func _process(_delta):
+ _update_mouse_position()
+ _update_hovered_block()
+
+func _input(event):
+ # Handle mouse clicks
+ if event is InputEventMouseButton:
+ if event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
+ _handle_place_block()
+ if event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
+ _handle_remove_block()
+
+func _unhandled_key_input(event):
+ if event.pressed and not event.echo:
+ match event.keycode:
+ KEY_1:
+ _select_block_with_height_toggle("grass")
+ KEY_2:
+ _select_block_with_height_toggle("stone")
+ KEY_3:
+ _select_block_with_height_toggle("water")
+ KEY_4:
+ _select_block_with_height_toggle("wood")
+ KEY_5:
+ _select_block_with_height_toggle("soil")
+ KEY_SPACE:
+ cycle_mode()
+ KEY_F:
+ _toggle_fog()
+ KEY_G:
+ _toggle_fog_globally()
+ KEY_Z:
+ if event.ctrl_pressed:
+ undo()
+ KEY_Y:
+ if event.ctrl_pressed:
+ redo()
+
+func _create_highlight_sprite():
+ highlight_sprite = Sprite2D.new()
+ # Add to renderer's world_layer instead of interaction system
+ if renderer:
+ renderer.world_layer.add_child(highlight_sprite)
+ else:
+ add_child(highlight_sprite)
+ highlight_sprite.z_index = 10000
+ highlight_sprite.centered = true
+
+func _update_mouse_position():
+ mouse_position = get_viewport().get_mouse_position()
+
+func _update_hovered_block():
+ if not camera:
+ return
+
+ # Convert screen position to world position
+ var world_pos_3d = camera.screen_to_world(mouse_position)
+ var block_pos = Vector3i(
+ roundi(world_pos_3d.x),
+ 0, # We'll check the topmost block
+ roundi(world_pos_3d.z)
+ )
+
+ # Find the highest block at this x,z position
+ if world_api:
+ var found_block = false
+ for y in range(10, -1, -1): # Check from top to bottom
+ block_pos.y = y
+ var block = world_api.get_block(block_pos)
+ if block:
+ hovered_position = block_pos
+ found_block = true
+ is_position_valid = true
+ break
+
+ if not found_block:
+ # No block found, hover over ground level
+ hovered_position = Vector3i(block_pos.x, 0, block_pos.z)
+ is_position_valid = true
+
+ # Update highlight position
+ _update_highlight()
+ block_hovered.emit(hovered_position)
+
+func _update_highlight():
+ if not is_position_valid or not highlight_sprite or not camera:
+ if highlight_sprite:
+ highlight_sprite.visible = false
+ return
+
+ highlight_sprite.visible = true
+
+ var iso_pos = camera.world_to_iso(Vector3(hovered_position.x, hovered_position.y, hovered_position.z))
+ highlight_sprite.position = iso_pos
+
+ # Use actual block texture with transparency based on mode
+ var alpha = 0.7
+ match current_mode:
+ InteractionMode.SELECT:
+ alpha = 0.5
+ InteractionMode.PLACE:
+ alpha = 0.8
+ InteractionMode.REMOVE:
+ alpha = 0.6
+
+ highlight_sprite.modulate = Color(1, 1, 1, alpha)
+
+func _handle_place_block():
+ if not world_api or not is_position_valid:
+ print("ERROR: world_api is null or position invalid!")
+ return
+
+ if current_mode == InteractionMode.PLACE:
+ var place_pos = hovered_position
+
+ # Place below cursor when hovering elevated blocks
+ if hovered_position.y > 0:
+ place_pos.y -= 1
+ else:
+ place_pos.y = 0
+
+ print("Placing ", selected_block_type, " at ", place_pos)
+
+ # Check if a block already exists at the target location
+ var existing_block = world_api.get_block(place_pos)
+ var old_block_type = existing_block.get_type_id() if existing_block else null
+ var old_block_height = existing_block.height if existing_block else 0.0
+
+ if existing_block:
+ if existing_block.get_type_id() == selected_block_type:
+ return
+ world_api.remove_block(place_pos)
+
+ var block_height = _get_selected_block_height()
+ world_api.add_block(place_pos, selected_block_type, block_height)
+
+ _record_action({
+ "type": "place",
+ "position": place_pos,
+ "block_type": selected_block_type,
+ "block_height": block_height,
+ "old_block_type": old_block_type,
+ "old_block_height": old_block_height
+ })
+
+ elif current_mode == InteractionMode.SELECT:
+ selected_position = hovered_position
+ block_selected.emit(selected_position)
+
+func _handle_remove_block():
+ if not world_api or not is_position_valid:
+ return
+
+ var block = world_api.get_block(hovered_position)
+ if block:
+ var block_type = block.get_type_id()
+ var block_height = block.height
+ world_api.remove_block(hovered_position)
+
+ _record_action({
+ "type": "remove",
+ "position": hovered_position,
+ "block_type": block_type,
+ "block_height": block_height
+ })
+
+func cycle_mode():
+ current_mode = (current_mode + 1) % 3 as InteractionMode
+ interaction_mode_changed.emit(current_mode)
+ _update_status_ui()
+
+func set_mode(mode: InteractionMode):
+ current_mode = mode
+ interaction_mode_changed.emit(current_mode)
+ _update_status_ui()
+
+func _update_status_ui():
+ if status_label:
+ var mode_text = InteractionMode.keys()[current_mode]
+ var block_text = selected_block_type.capitalize()
+ status_label.text = "Mode: " + mode_text + "\nBlock: " + block_text
+
+func _update_cursor_texture():
+ if not highlight_sprite:
+ return
+
+ # Load the actual block SVG texture for the cursor
+ var texture_path = "res://assets/blocks/isometric-" + selected_block_type + ".svg"
+ var texture = load(texture_path)
+ if texture:
+ highlight_sprite.texture = texture
+
+ # Scale cursor based on selected height
+ var display_height = _get_selected_block_height()
+ highlight_sprite.scale = Vector2(1, display_height)
+ else:
+ print("WARNING: Could not load cursor texture: ", texture_path)
+
+func _select_block_with_height_toggle(block_type: String):
+ # Check if same block pressed twice
+ if selected_block_type == block_type and last_selected_block == block_type:
+ # Toggle between default and full height
+ var default_height = world_api.get_block_type(block_type).height if world_api else 1.0
+ if selected_block_height_override < 0 or selected_block_height_override == default_height:
+ # Switch to full height
+ selected_block_height_override = 1.0
+ print("Selected: ", block_type.capitalize(), " (Full height)")
+ else:
+ # Switch back to default height
+ selected_block_height_override = -1.0
+ print("Selected: ", block_type.capitalize(), " (Default height)")
+ else:
+ # New block type selected, use default height
+ selected_block_type = block_type
+ selected_block_height_override = -1.0
+ print("Selected: ", block_type.capitalize())
+
+ last_selected_block = block_type
+ _update_cursor_texture()
+ _update_status_ui()
+
+func _get_selected_block_height() -> float:
+ # Return override height if set, otherwise use block type's default
+ if selected_block_height_override >= 0:
+ return selected_block_height_override
+ var block_type = world_api.get_block_type(selected_block_type) if world_api else null
+ return block_type.height if block_type else 1.0
+
+func _toggle_fog():
+ if fog_system:
+ # Reveal area around camera center
+ var center_pos = Vector3i(0, 0, 0)
+ if camera:
+ var viewport_size = get_viewport().get_visible_rect().size
+ var world_center = camera.screen_to_world(viewport_size / 2)
+ center_pos = Vector3i(roundi(world_center.x), 0, roundi(world_center.z))
+
+ fog_system.reveal_area(center_pos, fog_system.revelation_radius * 2)
+
+func _toggle_fog_globally():
+ if fog_system:
+ fog_system.toggle_fog_globally()
+
+func _record_action(action: Dictionary):
+ # Clear any redo history when a new action is recorded
+ if history_index < action_history.size() - 1:
+ action_history.resize(history_index + 1)
+
+ # Add the new action
+ action_history.append(action)
+ history_index += 1
+
+ # Limit history size
+ if action_history.size() > MAX_HISTORY_SIZE:
+ action_history.pop_front()
+ history_index = MAX_HISTORY_SIZE - 1
+
+func undo():
+ if history_index < 0 or action_history.is_empty():
+ return
+
+ var action = action_history[history_index]
+ history_index -= 1
+
+ match action["type"]:
+ "place":
+ # Undo a place: remove the placed block
+ world_api.remove_block(action["position"])
+ # If there was an old block, restore it with original height
+ if action["old_block_type"]:
+ var old_height = action.get("old_block_height", 0.5)
+ world_api.add_block(action["position"], action["old_block_type"], old_height)
+ print("Undone place at ", action["position"])
+
+ "remove":
+ # Undo a remove: restore the removed block with original height
+ var block_height = action.get("block_height", 0.5)
+ world_api.add_block(action["position"], action["block_type"], block_height)
+ print("Undone remove at ", action["position"])
+
+func redo():
+ if history_index >= action_history.size() - 1:
+ return
+
+ history_index += 1
+ var action = action_history[history_index]
+
+ match action["type"]:
+ "place":
+ # Redo a place: remove old block if any, then place new block with height
+ if action["old_block_type"]:
+ world_api.remove_block(action["position"])
+ var block_height = action.get("block_height", 0.5)
+ world_api.add_block(action["position"], action["block_type"], block_height)
+ print("Redone place at ", action["position"])
+
+ "remove":
+ # Redo a remove: remove the block again
+ world_api.remove_block(action["position"])
+ print("Redone remove at ", action["position"])
diff --git a/scripts/systems/InteractionSystem.gd.uid b/scripts/systems/InteractionSystem.gd.uid
new file mode 100644
index 0000000..6cf0a68
--- /dev/null
+++ b/scripts/systems/InteractionSystem.gd.uid
@@ -0,0 +1 @@
+uid://bav4mk0r2x4xt
diff --git a/scripts/systems/LightingSystem.gd b/scripts/systems/LightingSystem.gd
new file mode 100644
index 0000000..3fac346
--- /dev/null
+++ b/scripts/systems/LightingSystem.gd
@@ -0,0 +1,155 @@
+## OpenIsopix - Lighting System
+## Manages environmental and block-emitted lighting
+
+class_name LightingSystem
+extends Node
+
+@export var world_api: WorldAPI
+
+## Environmental lighting levels (0.0 to 1.0)
+enum EnvironmentalLevel {
+ PITCH_BLACK,
+ VERY_DARK,
+ DARK,
+ DIM,
+ NORMAL,
+ BRIGHT,
+ VERY_BRIGHT
+}
+
+const LIGHTING_VALUES = {
+ EnvironmentalLevel.PITCH_BLACK: 0.1,
+ EnvironmentalLevel.VERY_DARK: 0.2,
+ EnvironmentalLevel.DARK: 0.4,
+ EnvironmentalLevel.DIM: 0.6,
+ EnvironmentalLevel.NORMAL: 0.8,
+ EnvironmentalLevel.BRIGHT: 0.9,
+ EnvironmentalLevel.VERY_BRIGHT: 1.0
+}
+
+var current_environmental_level: EnvironmentalLevel = EnvironmentalLevel.NORMAL
+var base_light_level: float = 0.8
+
+## Light propagation queue
+var light_update_queue: Array[Vector3i] = []
+
+signal lighting_updated(position: Vector3i, light_level: float)
+
+func _ready():
+ if world_api:
+ world_api.block_added.connect(_on_block_added)
+ world_api.block_removed.connect(_on_block_removed)
+
+func _process(_delta):
+ # Process light updates in batches
+ var updates_per_frame = 100
+ while not light_update_queue.is_empty() and updates_per_frame > 0:
+ var pos = light_update_queue.pop_front()
+ _update_block_lighting(pos)
+ updates_per_frame -= 1
+
+## Set environmental lighting level
+func set_environmental_level(level: EnvironmentalLevel):
+ current_environmental_level = level
+ base_light_level = LIGHTING_VALUES[level]
+ _recalculate_all_lighting()
+
+## Calculate lighting for a specific block
+func calculate_block_lighting(world_pos: Vector3i) -> float:
+ var light_level = base_light_level
+
+ # Check for nearby light-emitting blocks
+ var nearby_light = _get_nearby_light_contribution(world_pos)
+ light_level = max(light_level, nearby_light)
+
+ # Check for occlusion (blocks above reducing light)
+ var occlusion = _calculate_occlusion(world_pos)
+ light_level *= (1.0 - occlusion * 0.5)
+
+ return clamp(light_level, 0.0, 1.0)
+
+func _get_nearby_light_contribution(world_pos: Vector3i) -> float:
+ if not world_api:
+ return 0.0
+
+ var max_light = 0.0
+ var search_radius = 5 # Search in a reasonable radius
+
+ for x in range(-search_radius, search_radius + 1):
+ for y in range(-search_radius, search_radius + 1):
+ for z in range(-search_radius, search_radius + 1):
+ var check_pos = world_pos + Vector3i(x, y, z)
+ var block = world_api.get_block(check_pos)
+
+ if block and block.block_type and block.block_type.emits_light:
+ var distance = world_pos.distance_to(check_pos)
+ var light_radius = block.block_type.light_radius / 32.0 # Convert to world units
+
+ if distance <= light_radius:
+ var attenuation = 1.0 - (distance / light_radius)
+ var contribution = block.block_type.light_intensity * attenuation
+ max_light = max(max_light, contribution)
+
+ return max_light
+
+func _calculate_occlusion(world_pos: Vector3i) -> float:
+ if not world_api:
+ return 0.0
+
+ var occlusion = 0.0
+ var check_height = 5 # Check up to 5 blocks above
+
+ for y in range(1, check_height + 1):
+ var check_pos = world_pos + Vector3i(0, y, 0)
+ var block = world_api.get_block(check_pos)
+
+ if block and block.block_type and block.block_type.is_opaque:
+ occlusion += 0.2 # Each opaque block above reduces light
+
+ return clamp(occlusion, 0.0, 1.0)
+
+func _update_block_lighting(world_pos: Vector3i):
+ var light_level = calculate_block_lighting(world_pos)
+
+ if world_api:
+ world_api.modify_block(world_pos, "light_level", light_level)
+
+ lighting_updated.emit(world_pos, light_level)
+
+func _recalculate_all_lighting():
+ if not world_api:
+ return
+
+ # Queue all loaded blocks for lighting update
+ for chunk_key in world_api.chunks.keys():
+ var chunk = world_api.chunks[chunk_key]
+ if chunk.is_loaded:
+ var blocks = chunk.get_all_blocks()
+ for block in blocks:
+ if block:
+ light_update_queue.append(block.position)
+
+func _propagate_light_from_source(world_pos: Vector3i):
+ # Add neighboring blocks to update queue
+ var neighbors = [
+ world_pos + Vector3i(1, 0, 0),
+ world_pos + Vector3i(-1, 0, 0),
+ world_pos + Vector3i(0, 1, 0),
+ world_pos + Vector3i(0, -1, 0),
+ world_pos + Vector3i(0, 0, 1),
+ world_pos + Vector3i(0, 0, -1),
+ ]
+
+ for neighbor in neighbors:
+ if neighbor not in light_update_queue:
+ light_update_queue.append(neighbor)
+
+## Signal handlers
+func _on_block_added(position: Vector3i, block: BlockInstance):
+ light_update_queue.append(position)
+ if block and block.block_type and block.block_type.emits_light:
+ _propagate_light_from_source(position)
+
+func _on_block_removed(position: Vector3i):
+ light_update_queue.append(position)
+ _propagate_light_from_source(position)
diff --git a/scripts/systems/LightingSystem.gd.uid b/scripts/systems/LightingSystem.gd.uid
new file mode 100644
index 0000000..b7102bb
--- /dev/null
+++ b/scripts/systems/LightingSystem.gd.uid
@@ -0,0 +1 @@
+uid://7rojcms7vus6