Spaces:
Running on Zero
Running on Zero
| """ | |
| 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) | |