GameConfigIdea / godot_exporter.py
kwabs22
Port changes from duplicate space to original
9328e91
Raw
History Blame Contribute Delete
28.7 kB
"""
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)