""" Godot Exporter - Converts game configs to Godot 4.x GDScript and Scene files. Generates a complete game with: - GameState (inventory, flags, counters, etc.) - Condition Evaluator (has_item, flag, money checks, etc.) - Effect Applicator (add_item, set_flag, increment, etc.) - Transition Resolver (random, conditional, chained) - 3D Scene with placeholder meshes for each location/choice - Game Controller for navigation and state management Target: Godot 4.x """ import json def generate_gamestate_gd(): """Generate GDScript GameState autoload singleton.""" return '''extends Node # ============================================================ # GAME STATE - Autoload Singleton # ============================================================ # Add this as an AutoLoad in Project Settings -> Autoload # Name it "GameState" signal state_changed var inventory: Array[String] = [] var money: int = 20 var flags: Dictionary = {} var counters: Dictionary = {} var people_met: Array[String] = [] var locations_visited: Array[String] = [] var current_location: String = "" var current_state: String = "" # ==================== Inventory ==================== func add_item(item: String) -> void: if item not in inventory: inventory.append(item) _log_event("effect_applied", "add_item=" + item) state_changed.emit() func remove_item(item: String) -> void: var idx = inventory.find(item) if idx >= 0: inventory.remove_at(idx) _log_event("effect_applied", "remove_item=" + item) state_changed.emit() func has_item(item: String) -> bool: return item in inventory func add_items(items: Array) -> void: for item in items: add_item(item) # ==================== Money ==================== func add_money(amount: int) -> void: money += amount _log_event("effect_applied", "add_money=" + str(amount)) state_changed.emit() func remove_money(amount: int) -> void: money -= amount _log_event("effect_applied", "remove_money=" + str(amount)) state_changed.emit() func set_money(amount: int) -> void: money = amount state_changed.emit() # ==================== Flags ==================== func set_flag(flag_name: String, value: bool = true) -> void: flags[flag_name] = value _log_event("effect_applied", "set_flag=" + flag_name + ":" + str(value)) state_changed.emit() func clear_flag(flag_name: String) -> void: flags[flag_name] = false state_changed.emit() func toggle_flag(flag_name: String) -> void: flags[flag_name] = not flags.get(flag_name, false) state_changed.emit() func has_flag(flag_name: String) -> bool: return flags.get(flag_name, false) # ==================== Counters ==================== func set_counter(counter_name: String, value: int) -> void: counters[counter_name] = value _log_event("effect_applied", "set_counter=" + counter_name + ":" + str(value)) state_changed.emit() func get_counter(counter_name: String) -> int: return counters.get(counter_name, 0) func increment_counter(counter_name: String, amount: int = 1) -> void: counters[counter_name] = counters.get(counter_name, 0) + amount _log_event("effect_applied", "increment=" + counter_name + ":" + str(amount)) state_changed.emit() func decrement_counter(counter_name: String, amount: int = 1) -> void: counters[counter_name] = counters.get(counter_name, 0) - amount state_changed.emit() # ==================== People ==================== func meet_person(person_name: String) -> void: if person_name not in people_met: people_met.append(person_name) _log_event("effect_applied", "meet_person=" + person_name) state_changed.emit() func has_met(person_name: String) -> bool: return person_name in people_met # ==================== Locations ==================== func visit_location(location_name: String) -> void: if location_name not in locations_visited: locations_visited.append(location_name) state_changed.emit() func has_visited(location_name: String) -> bool: return location_name in locations_visited func discover_location(location_name: String) -> void: visit_location(location_name) func has_discovered(location_name: String) -> bool: return has_visited(location_name) # ==================== State Summary ==================== func get_state_summary() -> String: var summary = "=== Game State ===\\n" summary += "Location: %s / %s\\n" % [current_location, current_state] summary += "Money: %d\\n" % money summary += "Inventory: %s\\n" % str(inventory) summary += "Flags: %s\\n" % str(flags) summary += "Counters: %s\\n" % str(counters) return summary # ==================== Event Logging ==================== func _log_event(event_type: String, details: String) -> void: print("[GAME_EVENT] %s: %s" % [event_type, details]) func reset() -> void: inventory.clear() money = 20 flags.clear() counters.clear() people_met.clear() locations_visited.clear() current_location = "" current_state = "" state_changed.emit() ''' def generate_condition_evaluator_gd(): """Generate GDScript Condition Evaluator.""" return '''extends Node # ============================================================ # CONDITION EVALUATOR # ============================================================ # Evaluates condition expressions against GameState func evaluate(condition) -> bool: # No condition = always true if condition == null or (condition is Dictionary and condition.is_empty()): return true # String = flag check if condition is String: return GameState.has_flag(condition) if not condition is Dictionary: return false # Compound conditions if condition.has("and"): for c in condition["and"]: if not evaluate(c): return false return true if condition.has("or"): for c in condition["or"]: if evaluate(c): return true return false if condition.has("not"): return not evaluate(condition["not"]) # Atomic conditions return _evaluate_atomic(condition) func _evaluate_atomic(condition: Dictionary) -> bool: # Inventory checks if condition.has("has_item"): return GameState.has_item(condition["has_item"]) if condition.has("not_has_item"): return not GameState.has_item(condition["not_has_item"]) # Flag checks if condition.has("flag"): return GameState.has_flag(condition["flag"]) if condition.has("not_flag"): return not GameState.has_flag(condition["not_flag"]) # Money checks if condition.has("money"): return _compare_numeric(GameState.money, condition["money"]) # Counter checks if condition.has("counter"): for counter_name in condition["counter"]: var value = GameState.get_counter(counter_name) if not _compare_numeric(value, condition["counter"][counter_name]): return false return true # People checks if condition.has("met_person"): return GameState.has_met(condition["met_person"]) if condition.has("not_met_person"): return not GameState.has_met(condition["not_met_person"]) # Location checks if condition.has("visited"): return GameState.has_visited(condition["visited"]) if condition.has("not_visited"): return not GameState.has_visited(condition["not_visited"]) return true func _compare_numeric(actual: int, comparison) -> bool: if comparison is int or comparison is float: return actual >= comparison if comparison is Dictionary: if comparison.has("gte"): return actual >= comparison["gte"] if comparison.has("gt"): return actual > comparison["gt"] if comparison.has("lte"): return actual <= comparison["lte"] if comparison.has("lt"): return actual < comparison["lt"] if comparison.has("eq"): return actual == comparison["eq"] if comparison.has("neq"): return actual != comparison["neq"] return true ''' def generate_effect_applicator_gd(): """Generate GDScript Effect Applicator.""" return '''extends Node # ============================================================ # EFFECT APPLICATOR # ============================================================ # Applies declarative effects to GameState func apply(effects: Dictionary) -> void: if effects.is_empty(): return # Inventory effects if effects.has("add_item"): var items = effects["add_item"] if items is Array: GameState.add_items(items) else: GameState.add_item(items) if effects.has("remove_item"): var items = effects["remove_item"] if items is Array: for item in items: GameState.remove_item(item) else: GameState.remove_item(items) # Money effects if effects.has("add_money"): GameState.add_money(effects["add_money"]) if effects.has("remove_money"): GameState.remove_money(effects["remove_money"]) if effects.has("set_money"): GameState.set_money(effects["set_money"]) # Flag effects if effects.has("set_flag"): var flag_data = effects["set_flag"] if flag_data is String: GameState.set_flag(flag_data) elif flag_data is Array: for f in flag_data: GameState.set_flag(f) elif flag_data is Dictionary: for f in flag_data: GameState.set_flag(f, flag_data[f]) if effects.has("clear_flag"): var flags = effects["clear_flag"] if flags is Array: for f in flags: GameState.clear_flag(f) else: GameState.clear_flag(flags) if effects.has("toggle_flag"): var flags = effects["toggle_flag"] if flags is Array: for f in flags: GameState.toggle_flag(f) else: GameState.toggle_flag(flags) # Counter effects if effects.has("set_counter"): for counter_name in effects["set_counter"]: GameState.set_counter(counter_name, effects["set_counter"][counter_name]) if effects.has("increment"): for counter_name in effects["increment"]: GameState.increment_counter(counter_name, effects["increment"][counter_name]) if effects.has("decrement"): for counter_name in effects["decrement"]: GameState.decrement_counter(counter_name, effects["decrement"][counter_name]) # People effects if effects.has("add_person"): var people = effects["add_person"] if people is Array: for p in people: GameState.meet_person(p) else: GameState.meet_person(people) # Location effects if effects.has("add_location"): var locs = effects["add_location"] if locs is Array: for loc in locs: GameState.discover_location(loc) else: GameState.discover_location(locs) if effects.has("visit_location"): var locs = effects["visit_location"] if locs is Array: for loc in locs: GameState.visit_location(loc) else: GameState.visit_location(locs) ''' def generate_transition_resolver_gd(): """Generate GDScript Transition Resolver.""" return '''extends Node # ============================================================ # TRANSITION RESOLVER # ============================================================ # Resolves dynamic transitions @onready var condition_evaluator = $"../ConditionEvaluator" func resolve(transition) -> String: # Simple string transition if transition is String: return transition if not transition is Dictionary: return "" # Weighted random if transition.has("random"): return _resolve_weighted_random(transition["random"]) # Equal probability pool if transition.has("random_from"): var pool = transition["random_from"] if pool.size() > 0: return pool[randi() % pool.size()] return "" # Conditional if/then/else if transition.has("if"): if condition_evaluator.evaluate(transition["if"]): if transition.has("then"): return resolve(transition["then"]) else: if transition.has("else"): return resolve(transition["else"]) return "" # Chained conditions if transition.has("conditions"): for cond_block in transition["conditions"]: if cond_block.has("default"): return resolve(cond_block["default"]) if cond_block.has("if") and condition_evaluator.evaluate(cond_block["if"]): return resolve(cond_block["then"]) return "" return "" func _resolve_weighted_random(weights: Array) -> String: if weights.size() == 0: return "" var total = 0.0 for w in weights: total += w[1] var roll = randf() * total for w in weights: roll -= w[1] if roll <= 0: return w[0] return weights[weights.size() - 1][0] ''' def generate_choice_script_gd(): """Generate GDScript for clickable choice boxes.""" return '''extends StaticBody3D # ============================================================ # CHOICE - Clickable choice box # ============================================================ signal choice_selected(choice_data) @export var choice_text: String = "" @export var choice_condition: Dictionary = {} @export var choice_transition = "" # Can be String or Dictionary @export var choice_effects: Dictionary = {} var _mesh_instance: MeshInstance3D var _label: Label3D var _default_material: StandardMaterial3D var _hover_material: StandardMaterial3D var _disabled_material: StandardMaterial3D func _ready(): # Get or create mesh instance _mesh_instance = get_node_or_null("MeshInstance3D") if not _mesh_instance: _mesh_instance = MeshInstance3D.new() _mesh_instance.mesh = BoxMesh.new() add_child(_mesh_instance) # Create materials _default_material = StandardMaterial3D.new() _default_material.albedo_color = Color(0.3, 0.5, 0.8) _hover_material = StandardMaterial3D.new() _hover_material.albedo_color = Color(0.5, 0.7, 1.0) _hover_material.emission_enabled = true _hover_material.emission = Color(0.2, 0.3, 0.5) _disabled_material = StandardMaterial3D.new() _disabled_material.albedo_color = Color(0.3, 0.3, 0.3, 0.5) _mesh_instance.material_override = _default_material # Create label _label = get_node_or_null("Label3D") if not _label: _label = Label3D.new() _label.position = Vector3(0, 1.5, 0) _label.font_size = 48 _label.billboard = BaseMaterial3D.BILLBOARD_ENABLED add_child(_label) _label.text = choice_text # Connect to state changes GameState.state_changed.connect(_on_state_changed) _update_visibility() func _on_state_changed(): _update_visibility() func _update_visibility(): var evaluator = get_node_or_null("/root/Main/ConditionEvaluator") if evaluator: var condition_met = evaluator.evaluate(choice_condition) visible = condition_met _mesh_instance.material_override = _default_material if condition_met else _disabled_material func _input_event(_camera, event, _position, _normal, _shape_idx): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: _on_clicked() func _on_clicked(): print("[GAME_EVENT] choice_selected: " + choice_text) choice_selected.emit({ "text": choice_text, "transition": choice_transition, "effects": choice_effects }) func set_hover(is_hovered: bool): if visible: _mesh_instance.material_override = _hover_material if is_hovered else _default_material ''' def generate_game_controller_gd(config): """Generate GDScript Game Controller.""" # Find starting location/state first_location = next(iter(config.keys())) first_state = next(iter(config[first_location].keys())) # Convert config to GDScript dictionary syntax config_json = json.dumps(config) config_json_escaped = config_json.replace("'", "\\'") return f'''extends Node3D # ============================================================ # GAME CONTROLLER # ============================================================ @onready var condition_evaluator = $ConditionEvaluator @onready var effect_applicator = $EffectApplicator @onready var transition_resolver = $TransitionResolver var game_config: Dictionary var location_nodes: Dictionary = {{}} var current_location_node: Node3D = null func _ready(): # Parse config game_config = JSON.parse_string('{config_json_escaped}') # Cache location nodes for child in get_children(): if child.name.begins_with("Loc_"): location_nodes[child.name.substr(4)] = child child.visible = false # Connect choice signals for choice_node in child.get_children(): if choice_node.has_signal("choice_selected"): choice_node.choice_selected.connect(_on_choice_selected) # Start game call_deferred("_start_game") func _start_game(): navigate_to("{first_location}", "{first_state}") func navigate_to(location: String, state: String): var entity_name = location + "_" + state print("[GAME_EVENT] transition: " + GameState.current_location + "_" + GameState.current_state + " -> " + entity_name) # Hide current if current_location_node: current_location_node.visible = false # Show new if location_nodes.has(entity_name): current_location_node = location_nodes[entity_name] current_location_node.visible = true # Update state GameState.current_location = location GameState.current_state = state GameState.visit_location(location) # Apply on_enter effects var state_data = _get_state_data(location, state) if state_data and state_data.has("on_enter"): effect_applicator.apply(state_data["on_enter"]) # Update UI _update_description(state_data) else: push_error("Location not found: " + entity_name) func _get_state_data(location: String, state: String): if game_config.has(location) and game_config[location].has(state): return game_config[location][state] return null func _update_description(state_data): if state_data and state_data.has("description"): print("Description: " + state_data["description"]) # You can connect this to a UI label func _on_choice_selected(choice_data: Dictionary): # Apply effects if choice_data.has("effects"): effect_applicator.apply(choice_data["effects"]) # Resolve and execute transition if choice_data.has("transition"): var target = transition_resolver.resolve(choice_data["transition"]) if target != "": var parts = target.rsplit("_", true, 1) if parts.size() >= 2: navigate_to(parts[0], parts[1]) func _input(event): # Debug: Print state on F1 if event is InputEventKey and event.pressed and event.keycode == KEY_F1: print(GameState.get_state_summary()) ''' def generate_scene_tscn(config): """Generate Godot .tscn scene file with all locations and choices.""" lines = [] # Count resources needed resource_count = 3 # BoxMesh, ground mesh, materials node_count = 1 # Root for location, states in config.items(): if not isinstance(states, dict): continue for state_name, state_data in states.items(): if not isinstance(state_data, dict): continue node_count += 1 # Location node choices = state_data.get('choices', []) node_count += len(choices) + 1 # Choices + ground # Header lines.append('[gd_scene load_steps=%d format=3]' % (resource_count + 5)) lines.append('') # External scripts lines.append('[ext_resource type="Script" path="res://game_controller.gd" id="1"]') lines.append('[ext_resource type="Script" path="res://condition_evaluator.gd" id="2"]') lines.append('[ext_resource type="Script" path="res://effect_applicator.gd" id="3"]') lines.append('[ext_resource type="Script" path="res://transition_resolver.gd" id="4"]') lines.append('[ext_resource type="Script" path="res://choice.gd" id="5"]') lines.append('') # Sub-resources lines.append('[sub_resource type="BoxMesh" id="BoxMesh_choice"]') lines.append('') lines.append('[sub_resource type="BoxShape3D" id="BoxShape_choice"]') lines.append('') lines.append('[sub_resource type="PlaneMesh" id="PlaneMesh_ground"]') lines.append('size = Vector2(15, 15)') lines.append('') # Root node lines.append('[node name="Main" type="Node3D"]') lines.append('script = ExtResource("1")') lines.append('') # Helper nodes lines.append('[node name="ConditionEvaluator" type="Node" parent="."]') lines.append('script = ExtResource("2")') lines.append('') lines.append('[node name="EffectApplicator" type="Node" parent="."]') lines.append('script = ExtResource("3")') lines.append('') lines.append('[node name="TransitionResolver" type="Node" parent="."]') lines.append('script = ExtResource("4")') lines.append('') # Camera lines.append('[node name="Camera3D" type="Camera3D" parent="."]') lines.append('transform = Transform3D(1, 0, 0, 0, 0.906308, 0.422618, 0, -0.422618, 0.906308, 0, 5, 10)') lines.append('') # Light lines.append('[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]') lines.append('transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 10, 0)') lines.append('') # Generate location nodes state_index = 0 for location, states in config.items(): if not isinstance(states, dict): continue for state_name, state_data in states.items(): if not isinstance(state_data, dict): continue entity_name = f"{location}_{state_name}" safe_name = entity_name.replace('-', '_').replace(' ', '_') x_pos = state_index * 25 # Location container node lines.append(f'[node name="Loc_{safe_name}" type="Node3D" parent="."]') lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x_pos}, 0, 0)') lines.append('') # Ground plane lines.append(f'[node name="Ground" type="MeshInstance3D" parent="Loc_{safe_name}"]') lines.append('mesh = SubResource("PlaneMesh_ground")') lines.append('') # Choices choices = state_data.get('choices', []) choice_config = state_data.get('choice_config', {}) transitions = state_data.get('transitions', {}) effects = state_data.get('effects', {}) for idx, choice in enumerate(choices): choice_safe = choice.replace('-', '_').replace(' ', '_').replace("'", "")[:20] x_offset = (idx - len(choices)/2) * 3 # Get choice data condition = choice_config.get(choice, {}).get('condition', {}) transition = transitions.get(choice, '') effect = effects.get(choice, {}) lines.append(f'[node name="Choice_{choice_safe}_{idx}" type="StaticBody3D" parent="Loc_{safe_name}"]') lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x_offset}, 1, -5)') lines.append('script = ExtResource("5")') lines.append(f'choice_text = "{choice}"') # Serialize condition, transition, effects as metadata if condition: lines.append(f'choice_condition = {_dict_to_gdscript(condition)}') if isinstance(transition, dict): lines.append(f'choice_transition = {_dict_to_gdscript(transition)}') elif transition: lines.append(f'choice_transition = "{transition}"') if effect: lines.append(f'choice_effects = {_dict_to_gdscript(effect)}') lines.append('') # Mesh for the choice lines.append(f'[node name="MeshInstance3D" type="MeshInstance3D" parent="Loc_{safe_name}/Choice_{choice_safe}_{idx}"]') lines.append('mesh = SubResource("BoxMesh_choice")') lines.append('') # Collision shape lines.append(f'[node name="CollisionShape3D" type="CollisionShape3D" parent="Loc_{safe_name}/Choice_{choice_safe}_{idx}"]') lines.append('shape = SubResource("BoxShape_choice")') lines.append('') state_index += 1 return '\n'.join(lines) def _dict_to_gdscript(d): """Convert Python dict to GDScript dictionary literal.""" if isinstance(d, dict): pairs = [] for k, v in d.items(): pairs.append(f'"{k}": {_dict_to_gdscript(v)}') return '{' + ', '.join(pairs) + '}' elif isinstance(d, list): items = [_dict_to_gdscript(item) for item in d] return '[' + ', '.join(items) + ']' elif isinstance(d, str): return f'"{d}"' elif isinstance(d, bool): return 'true' if d else 'false' elif isinstance(d, (int, float)): return str(d) else: return 'null' def generate_project_godot_snippet(): """Generate project.godot autoload settings snippet.""" return '''# Add this to your project.godot under [autoload] # Or go to Project -> Project Settings -> Autoload and add: [autoload] GameState="*res://game_state.gd" ''' def export_to_godot(config_json): """ Main export function - converts game config to Godot 4.x files. Args: config_json: JSON string of the game config Returns: tuple: (explanation, dict of filename -> content) """ try: config = json.loads(config_json) except json.JSONDecodeError as e: return f"JSON Error: {e}", {} # Generate all files files = { 'game_state.gd': generate_gamestate_gd(), 'condition_evaluator.gd': generate_condition_evaluator_gd(), 'effect_applicator.gd': generate_effect_applicator_gd(), 'transition_resolver.gd': generate_transition_resolver_gd(), 'choice.gd': generate_choice_script_gd(), 'game_controller.gd': generate_game_controller_gd(config), 'main.tscn': generate_scene_tscn(config), '_README_SETUP.txt': generate_project_godot_snippet(), } # Create combined output for display combined = [] combined.append("=" * 60) combined.append("GODOT 4.x GAME EXPORT") combined.append("=" * 60) combined.append("") for filename, content in files.items(): combined.append(f"{'=' * 20} {filename} {'=' * 20}") combined.append(content) combined.append("") explanation = """Godot 4.x Export Generated! Instructions: 1. Create a new Godot 4.x project 2. Copy each generated file to your project's res:// folder: - game_state.gd (AutoLoad singleton) - condition_evaluator.gd - effect_applicator.gd - transition_resolver.gd - choice.gd - game_controller.gd - main.tscn (main scene) 3. Set up AutoLoad: Project -> Project Settings -> Autoload - Add game_state.gd as "GameState" (enable checkbox) 4. Set main.tscn as the main scene 5. Run the project The generated code includes: - GameState: Singleton tracking inventory, money, flags, counters - Condition Evaluator: Checks conditions (has_item, flag, money, etc.) - Effect Applicator: Applies effects (add_item, set_flag, increment, etc.) - Transition Resolver: Handles random/conditional transitions - Choice Script: Clickable 3D boxes with input handling - Game Controller: Navigation, state management, event logging Press F1 in-game to print the current state summary. Event logs use [GAME_EVENT] prefix for easy parsing. """ return explanation, '\n'.join(combined)