Starter-Kit-City-Builder/scripts/javascript_bridge.gd

541 lines
17 KiB
GDScript

extends Node
class_name JSBridge
# This script provides a bridge to JavaScript functionality
# while gracefully handling platforms that don't support it
# Check if JavaScript is available
static func has_interface() -> bool:
# Check if running in a web environment
# Use OS.has_feature("web") for consistency with sound_manager.gd
if OS.has_feature("web"):
print("Running in web environment, JavaScript should be available")
# Double-check by evaluating a simple script
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var test_result = js.eval("!!window && typeof window !== 'undefined'")
print("JavaScript test result: " + str(test_result))
return test_result != null
else:
print("JavaScriptBridge singleton not available, running in editor or non-web platform")
else:
print("Not running in web environment")
return false
# Get the JavaScript interface
static func get_interface():
if has_interface():
return JavaScriptGlobal
return null
# JavaScriptGlobal is a mock class that provides fallback implementations
# for platforms that don't support JavaScript
class JavaScriptGlobal:
# Check if a JavaScript function exists
static func has_function(function_name: String) -> bool:
if not OS.has_feature("web"):
return false
print("Checking if function exists: " + function_name)
var script = "typeof %s === 'function'" % function_name
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var result = js.eval(script)
# If result is null, the JavaScript eval failed
if result == null:
print("JavaScript eval failed when checking for function: " + function_name)
return false
print("Function check result for " + function_name + ": " + str(result))
return result
else:
print("JavaScriptBridge singleton not available")
return false
# Evaluate JavaScript code
static func eval(script: String):
if not OS.has_feature("web"):
return null
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return null
# Call a JavaScript function with arguments
static func call_js_function(function_name: String, args = []):
if not OS.has_feature("web"):
return null
var formatted_args = []
for arg in args:
if arg is String:
formatted_args.append("\"%s\"" % arg.replace("\"", "\\\""))
elif arg is Dictionary or arg is Array:
formatted_args.append(JSON.stringify(arg))
else:
formatted_args.append(str(arg))
var script = "%s(%s)" % [function_name, ",".join(formatted_args)]
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return null
# Connect to the learning companion - legacy method with postMessage fallback
static func connectLearningCompanion(success_callback = null, error_callback = null):
print("Attempting to connect to learning companion")
if not OS.has_feature("web"):
print("Skipping learning companion connection on non-web platform")
if error_callback != null and error_callback.is_valid():
error_callback.call()
return
# Always use postMessage approach regardless of function availability
connectLearningCompanionViaPostMessage(success_callback, error_callback)
# Connect to the learning companion using only postMessage
static func connectLearningCompanionViaPostMessage(success_callback = null, error_callback = null):
print("Connecting to learning companion via postMessage")
if not OS.has_feature("web"):
print("Skipping learning companion connection on non-web platform")
if error_callback != null and error_callback.is_valid():
error_callback.call()
return
# Use postMessage approach exclusively - note: no return statements allowed in the script
var script = """
(function() {
try {
// Send a message directly to the parent window
if (window.parent) {
console.log('Sending connection message to parent window');
window.parent.postMessage({
type: 'stemCity_connect',
source: 'godot-game',
timestamp: Date.now()
}, '*');
// Set up a global event listener for responses if not already set up
if (!window._stemCityListenerInitialized) {
window._stemCityListenerInitialized = true;
window.addEventListener('message', function(event) {
console.log('Game received message:', event.data);
if (event.data && event.data.type === 'stemCity_connect_ack') {
console.log('Received connection acknowledgment from parent');
}
});
}
// Don't use return statements here - they're not allowed in top-level eval
var result = true;
} else {
console.log('No parent window found');
var result = false;
}
} catch (e) {
console.error('Error connecting via postMessage:', e);
var result = false;
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
# Always consider this a success - we'll use the force connection timer as backup
print("Sent connection message via postMessage")
# Try to ensure audio is initialized as well since we now have user interaction
JavaScriptGlobal.ensure_audio_initialized()
if success_callback != null and success_callback.is_valid():
success_callback.call()
else:
print("JavaScriptBridge singleton not available")
if error_callback != null and error_callback.is_valid():
error_callback.call()
# The following methods call the JavaScript functions for game events using postMessage
static func onGameStarted():
if not OS.has_feature("web"):
return
print("Sending game started event via postMessage")
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending gameStarted message to parent window');
window.parent.postMessage({
type: 'stemCity_gameStarted',
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for gameStarted event');
}
} catch (e) {
console.error('Error sending gameStarted via postMessage:', e);
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onMissionStarted(mission_data: Dictionary):
if not OS.has_feature("web"):
return
print("Sending mission started event for mission: " + str(mission_data.get("id", "unknown")))
var mission_json = JSON.stringify(mission_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending missionStarted message to parent window');
window.parent.postMessage({
type: 'stemCity_missionStarted',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for missionStarted event');
}
} catch (e) {
console.error('Error sending missionStarted via postMessage:', e);
}
})();
""" % mission_json
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onMissionCompleted(mission_data: Dictionary):
if not OS.has_feature("web"):
return
print("Sending mission completed event for mission: " + str(mission_data.get("id", "unknown")))
var mission_json = JSON.stringify(mission_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending missionCompleted message to parent window');
window.parent.postMessage({
type: 'stemCity_missionCompleted',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for missionCompleted event');
}
} catch (e) {
console.error('Error sending missionCompleted via postMessage:', e);
}
})();
""" % mission_json
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onAllMissionsCompleted():
if not OS.has_feature("web"):
return
print("Sending all missions completed event")
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending allMissionsCompleted message to parent window');
window.parent.postMessage({
type: 'stemCity_allMissionsCompleted',
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for allMissionsCompleted event');
}
} catch (e) {
console.error('Error sending allMissionsCompleted via postMessage:', e);
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func sendCompanionDialog(key: String, dialog_data: Dictionary):
if not OS.has_feature("web"):
return
print("Sending companion dialog for key: " + key)
var dialog_json = JSON.stringify(dialog_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending companion dialog to parent window');
window.parent.postMessage({
type: 'stemCity_companionDialog',
key: '%s',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for companion dialog');
}
} catch (e) {
console.error('Error sending companion dialog via postMessage:', e);
}
})();
""" % [key, dialog_json]
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
# Handle audio actions via JavaScript
static func handle_audio_action(action: String, sound_name: String = "", volume: float = -1.0):
if not OS.has_feature("web"):
return false
print("Handling audio action via JavaScript bridge: " + action)
var action_data = {
"action": action,
"sound": sound_name,
}
if volume >= 0.0:
action_data["volume"] = volume
var action_json = JSON.stringify(action_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending audio action to parent window:', %s);
window.parent.postMessage({
type: 'stemCity_audio',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
} else {
console.log('No parent window found for audio action');
return false;
}
} catch (e) {
console.error('Error sending audio action via postMessage:', e);
return false;
}
})();
""" % [action_json, action_json]
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return false
# Helper method to ensure the sound manager's audio is initialized
# Call this method after user interaction to ensure audio works
static func ensure_audio_initialized():
if not OS.has_feature("web"):
return true # Audio always works on non-web platforms
print("Ensuring audio is initialized via JavaScript bridge")
# Setup audio message listener if it's not already set up
setup_audio_message_listener()
# Try to initialize audio through the sound manager if it exists
var sound_manager = _get_sound_manager()
if sound_manager and sound_manager.has_method("init_web_audio_from_js"):
print("Found SoundManager, calling init_web_audio_from_js")
sound_manager.init_web_audio_from_js()
# Follow up with direct JavaScript audio context unlocking for extra reliability
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
_run_audio_unlock_script(js)
return true
else:
# Fallback: directly try to unlock web audio using JavaScript
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var result = _run_audio_unlock_script(js)
return result
else:
print("JavaScriptBridge singleton not available for audio initialization")
return false
# Helper method to run the audio unlocking script with maximum compatibility
static func _run_audio_unlock_script(js_interface):
var script = """
(function() {
var result = false;
try {
// Simple approach to unlock audio
console.log('Running simplified audio unlock');
// Create audio context if needed
if (!window._godotAudioContext) {
window._godotAudioContext = new (window.AudioContext || window.webkitAudioContext)();
}
var audioCtx = window._godotAudioContext;
console.log('Audio context state:', audioCtx.state);
// Resume it (for Chrome/Safari)
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// Play a short, quiet beep
var oscillator = audioCtx.createOscillator();
var gainNode = audioCtx.createGain();
gainNode.gain.value = 0.01; // Very quiet
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(0);
oscillator.stop(0.1);
// Add event listeners for future interactions
['click', 'touchstart', 'touchend'].forEach(function(event) {
document.addEventListener(event, function() {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}, {once: false});
});
result = audioCtx.state === 'running';
} catch (e) {
console.error("JavaScript bridge: Audio unlock error:", e);
result = false;
}
return result;
})()
"""
# var result = js_interface.eval(script)
# print("JavaScript audio initialization result:", result)
# return result
# Setup audio message listener from JavaScript
static func setup_audio_message_listener():
if not OS.has_feature("web"):
return false
print("Setting up audio message listener via JavaScript bridge")
if not Engine.has_singleton("JavaScriptBridge"):
print("JavaScriptBridge singleton not available")
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Register the callback function
js.set_callback("godot_audio_callback", Callable(_get_sound_manager(), "process_js_audio_state"))
# Set up a listener for audio state messages
var script = """
(function() {
// Set up message listener if not already done
if (!window.godot_audio_listener_initialized) {
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'stemCity_audio_state') {
console.log('Godot received audio state:', event.data);
// Call our Godot callback with the state data
if (typeof godot_audio_callback === 'function') {
console.log('Sending audio state to Godot');
godot_audio_callback(event.data.data);
} else {
console.warn('godot_audio_callback is not available');
}
}
});
console.log('Audio message listener initialized');
window.godot_audio_listener_initialized = true;
// Request initial audio state from parent
window.parent.postMessage({
type: 'stemCity_audio',
data: { action: 'GET_STATE' },
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
})();
"""
var result = js.eval(script)
print("Audio message listener setup result: ", result)
return result
# Helper to get the sound manager instance
static func _get_sound_manager():
var sound_manager = null
# Try to find using meta
if Engine.get_main_loop().has_meta("sound_manager"):
sound_manager = Engine.get_main_loop().get_meta("sound_manager")
else:
# Try to find in scene tree
var scene_tree = Engine.get_main_loop() as SceneTree
if scene_tree:
sound_manager = scene_tree.root.get_node_or_null("/root/SoundManager")
return sound_manager