extends Node2D var editMode = true #Node Reference @onready var origin = $OriginMotion/Origin @onready var camera = $Camera2D @onready var controlPanel = $ControlPanel @onready var editControls = $EditControls @onready var tutorial = $Tutorial @onready var spriteViewer = $EditControls/SpriteViewer @onready var viewerArrows = $ViewerArrows @onready var spriteList = $EditControls/SpriteList @onready var fileDialog = $FileDialog @onready var replaceDialog = $ReplaceDialog @onready var saveDialog = $SaveDialog @onready var loadDialog = $LoadDialog @onready var lines = $Lines @onready var settingsMenu = $ControlPanel/SettingsMenu @onready var pushUpdates = $PushUpdates @onready var shadow = $shadowSprite @onready var ndi_output = $NDIOutput #Scene Reference @onready var spriteObject = preload("res://ui_scenes/selectedSprite/spriteObject.tscn") var saveLoaded = false #Motion var yVel = 0 var bounceSlider = 250 var bounceGravity = 1000 # Physics timing multiplier - adjust this to fine-tune physics speed # 1.0 = very slow, 10.0 = reasonable, 60.0 = old frame-based speed var physicsTimeMultiplier = 6.0 #Costumes var costume = 1 var bounceOnCostumeChange = false #Zooming var scaleOverall = 100 #Camera Panning var isPanning = false var initialPanPosition = Vector2.ZERO var initialCameraOffset = Vector2.ZERO var cameraOffset = Vector2.ZERO #View Mode Panning var viewModePanning = false var initialViewPanPosition = Vector2.ZERO var viewPanOffset = Vector2.ZERO var viewPanLerpSpeed = 3.0 var bounceChange = 0.0 #IMPORTANT var fileSystemOpen = false #background input capture signal emptiedCapture signal pressedKey var costumeKeys = ["1","2","3","4","5","6","7","8","9","0"] signal spriteVisToggles(keysPressed:Array) signal fatfuckingballs func _ready(): Global.main = self Global.fail = $Failed var newUser = Saving.settings["newUser"] Global.connect("startSpeaking",onSpeak) # Connect to StreamDeck if available var streamdeck_node = get_node_or_null("/root/ElgatoStreamDeck") if streamdeck_node != null: streamdeck_node.on_key_down.connect(changeCostumeStreamDeck) if newUser: loadDefaultAvatar() else: var lastAvatar = Saving.settings.get("lastAvatar", "") if lastAvatar != null and lastAvatar != "" and typeof(lastAvatar) == TYPE_STRING and FileAccess.file_exists(lastAvatar): loadAvatar(lastAvatar) else: loadDefaultAvatar() $ControlPanel/volumeSlider.value = Saving.settings["volume"] $ControlPanel/sensitiveSlider.value = Saving.settings["sense"] get_window().size = str_to_var(Saving.settings["windowSize"]) if Saving.settings.has("bounce"): bounceSlider = Saving.settings["bounce"] else: Saving.settings["bounce"] = 250 if Saving.settings.has("maxFPS"): Engine.max_fps = Saving.settings["maxFPS"] else: Saving.settings["maxFPS"] = 60 if Saving.settings.has("backgroundColor"): Global.backgroundColor = str_to_var(Saving.settings["backgroundColor"]) else: Saving.settings["backgroundColor"] = var_to_str(Color(0.0,0.0,0.0,0.0)) if Saving.settings.has("filtering"): Global.filtering = Saving.settings["filtering"] else: Saving.settings["filtering"] = false if Saving.settings.has("gravity"): bounceGravity = Saving.settings["gravity"] else: Saving.settings["gravity"] = 1000 if Saving.settings.has("costumeKeys"): costumeKeys = Saving.settings["costumeKeys"] else: Saving.settings["costumeKeys"] = costumeKeys if Saving.settings.has("blinkSpeed"): Global.blinkSpeed = Saving.settings["blinkSpeed"] else: Saving.settings["blinkSpeed"] = 1.0 if Saving.settings.has("blinkChance"): Global.blinkChance = Saving.settings["blinkChance"] else: Saving.settings["blinkChance"] = 200 if Saving.settings.has("bounceOnCostumeChange"): bounceOnCostumeChange = Saving.settings["bounceOnCostumeChange"] else: Saving.settings["bounceOnCostumeChange"] = false saveLoaded = true RenderingServer.set_default_clear_color(Global.backgroundColor) swapMode() settingsMenu.setvalues() changeCostume(1) var s = get_viewport().get_visible_rect().size origin.position = s*0.5 updateCameraPosition() # Test NDI output on startup call_deferred("toggle_ndi") func _process(delta): var hold = origin.get_parent().position.y origin.get_parent().position.y += yVel * delta * physicsTimeMultiplier if origin.get_parent().position.y > 0: origin.get_parent().position.y = 0 bounceChange = hold - origin.get_parent().position.y yVel += bounceGravity * delta * physicsTimeMultiplier # Handle view mode panning lerp back to center if !editMode: if !viewModePanning: # Lerp back to center when not actively panning viewPanOffset = viewPanOffset.lerp(Vector2.ZERO, viewPanLerpSpeed * delta) # Apply view pan offset to the root character (affects physics) origin.position = (get_viewport().get_visible_rect().size * 0.5) + viewPanOffset updateCameraPosition() if Input.is_action_just_pressed("openFolder"): OS.shell_open(ProjectSettings.globalize_path("user://")) moveSpriteMenu(delta) zoomScene(delta) fileSystemOpen = isFileSystemOpen() followShadow() ## Handle input events, especially for camera panning func _input(event): # Handle middle mouse button for camera panning/reset if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE: if editMode: if event.pressed: # Start panning in edit mode isPanning = true initialPanPosition = event.global_position initialCameraOffset = cameraOffset else: # Stop panning isPanning = false else: if event.pressed: # Start view mode panning viewModePanning = true initialViewPanPosition = event.global_position else: # Stop view mode panning viewModePanning = false # Handle mouse movement during edit mode panning elif editMode and event is InputEventMouseMotion and isPanning: var totalDelta = event.global_position - initialPanPosition # Scale pan distance based on zoom level for more intuitive panning # Higher zoom (closer) = less movement, lower zoom (farther) = more movement var zoomScale = 1.0 / camera.zoom.x var scaledDelta = totalDelta * zoomScale # Calculate new camera offset from initial position plus scaled movement cameraOffset = initialCameraOffset - scaledDelta # Update camera position immediately var s = get_viewport().get_visible_rect().size camera.position = origin.position + cameraOffset # Update UI positions controlPanel.position = camera.position + (s/(camera.zoom*2.0)) tutorial.position = controlPanel.position editControls.position = camera.position - (s/(camera.zoom*2.0)) viewerArrows.position = editControls.position pushUpdates.position.y = controlPanel.position.y pushUpdates.position.x = editControls.position.x # Handle mouse movement during view mode panning elif !editMode and event is InputEventMouseMotion and viewModePanning: var totalDelta = event.global_position - initialViewPanPosition # Scale pan distance for view mode (less sensitive than edit mode) # Invert the movement so dragging right moves character right (not left) var panSensitivity = 0.5 viewPanOffset = -totalDelta * panSensitivity # Handle NDI output hotkeys elif event is InputEventKey and event.pressed: # F9 key to toggle NDI streaming if event.keycode == KEY_F9: toggle_ndi_streaming() # F10 key to restart NDI streaming with new settings elif event.keycode == KEY_F10: restart_ndi_streaming() ## Toggle NDI streaming on/off func toggle_ndi_streaming(): if ndi_output: print("NDI Output is available - Source: ", ndi_output.name) Global.pushUpdate("NDI Output available - Source: " + ndi_output.name) else: print("NDI Output not found") Global.pushUpdate("NDI Output not found") ## Restart NDI streaming to apply any new settings func restart_ndi_streaming(): if ndi_output: print("NDI Output available - Source: ", ndi_output.name) Global.pushUpdate("NDI Output available - Source: " + ndi_output.name) else: print("NDI Output not found") Global.pushUpdate("NDI Output not found") func followShadow(): # Shadow disabled for selected sprites shadow.visible = false func isFileSystemOpen(): for obj in [replaceDialog,fileDialog,saveDialog,loadDialog]: if obj.visible: if obj == replaceDialog: return true Global.heldSprite = null return true # Check wobble sync group dialog if Global.spriteEdit and Global.spriteEdit.wobbleSyncControl and Global.spriteEdit.wobbleSyncControl.createGroupDialog and Global.spriteEdit.wobbleSyncControl.createGroupDialog.visible: return true return false #Displays control panel whether or not application is focused func _notification(what): match what: SceneTree.NOTIFICATION_APPLICATION_FOCUS_OUT: controlPanel.visible = false pushUpdates.visible = false SceneTree.NOTIFICATION_APPLICATION_FOCUS_IN: if !editMode: controlPanel.visible = true pushUpdates.visible = true 30: onWindowSizeChange() func onWindowSizeChange(): if !saveLoaded: return Saving.settings["windowSize"] = var_to_str(get_window().size) var s = get_viewport().get_visible_rect().size origin.position = s*0.5 lines.position = s*0.5 lines.drawLine() updateCameraPosition() spriteList.position.x = s.x - 233 func zoomScene(delta): #Handles Zooming if Input.is_action_pressed("control"): if Input.is_action_just_pressed("scrollUp"): if scaleOverall < 400: camera.zoom += Vector2(0.1,0.1) scaleOverall += 10 changeZoom() if Input.is_action_just_pressed("scrollDown"): if scaleOverall > 10: camera.zoom -= Vector2(0.1,0.1) scaleOverall -= 10 changeZoom() $ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a, 0.0, delta * (physicsTimeMultiplier * 1.2)) func changeZoom(): var newZoom = Vector2(1.0,1.0) / camera.zoom controlPanel.scale = newZoom tutorial.scale = newZoom editControls.scale = newZoom viewerArrows.scale = newZoom lines.scale = newZoom pushUpdates.scale = newZoom Global.mouse.scale = newZoom # Update UI positions to account for new zoom level # Force update even during panning since zoom affects UI positioning calculations var s = get_viewport().get_visible_rect().size controlPanel.position = camera.position + (s/(camera.zoom*2.0)) tutorial.position = controlPanel.position editControls.position = camera.position - (s/(camera.zoom*2.0)) viewerArrows.position = editControls.position pushUpdates.position.y = controlPanel.position.y pushUpdates.position.x = editControls.position.x $ControlPanel/ZoomLabel.modulate.a = 6.0 $ControlPanel/ZoomLabel.text = "Zoom : " + str(scaleOverall) + "%" Global.pushUpdate("Set zoom to " + str(scaleOverall) + "%") ## Update camera position with current offset func updateCameraPosition(): var s = get_viewport().get_visible_rect().size # Only update if not currently panning to avoid conflicts if !isPanning: camera.position = origin.position + cameraOffset # Update UI element positions relative to new camera position controlPanel.position = camera.position + (s/(camera.zoom*2.0)) tutorial.position = controlPanel.position editControls.position = camera.position - (s/(camera.zoom*2.0)) viewerArrows.position = editControls.position pushUpdates.position.y = controlPanel.position.y pushUpdates.position.x = editControls.position.x ## Reset camera position to center func resetCameraPosition(): cameraOffset = Vector2.ZERO isPanning = false viewPanOffset = Vector2.ZERO viewModePanning = false updateCameraPosition() Global.pushUpdate("Camera reset to center.") #When the user speaks! func onSpeak(): if origin.get_parent().position.y > -16: yVel = bounceSlider * -1 #Swaps between edit mode and view mode func swapMode(): Global.heldSprite = null editMode = !editMode Global.pushUpdate("Toggled editing mode.") # Reset camera when entering view mode if !editMode: resetCameraPosition() else: # Reset view mode panning when entering edit mode viewPanOffset = Vector2.ZERO viewModePanning = false get_viewport().transparent_bg = !editMode if Global.backgroundColor.a != 0.0: get_viewport().transparent_bg = false RenderingServer.set_default_clear_color(Global.backgroundColor) #processing editControls.set_process(editMode) controlPanel.set_process(!editMode) #visibility editControls.visible = editMode tutorial.visible = editMode controlPanel.visible = !editMode lines.visible = editMode spriteList.visible = editMode #Adds sprite object to scene func add_image(path): var rand = RandomNumberGenerator.new() var id = rand.randi() var sprite = spriteObject.instantiate() sprite.path = path sprite.id = id origin.add_child(sprite) sprite.position = Vector2.ZERO Global.spriteList.updateData() Global.pushUpdate("Added new sprite.") #Opens File Dialog func _on_add_button_pressed(): fileDialog.visible = true #Runs when selecting image in File Dialog func _on_file_dialog_file_selected(path): add_image(path) func _on_save_button_pressed(): $SaveDialog.visible = true func _on_load_button_pressed(): $LoadDialog.visible = true #LOAD AVATAR func _on_load_dialog_file_selected(path): # Handle case where path might be a Dictionary instead of string if typeof(path) == TYPE_DICTIONARY: push_error("Expected string path but got Dictionary: " + str(path)) return var data = Saving.read_save(path) if data == null: return origin.queue_free() var new = Node2D.new() $OriginMotion.add_child(new) origin = new for item in data: if typeof(data[item]) != TYPE_DICTIONARY: continue if not data[item].has("path"): continue var sprite = spriteObject.instantiate() sprite.path = data[item]["path"] # Load display name (with backward compatibility) if data[item].has("displayName"): sprite.displayName = data[item]["displayName"] sprite.id = data[item]["identification"] sprite.parentId = data[item]["parentId"] sprite.offset = str_to_var(data[item]["offset"]) sprite.z = data[item]["zindex"] sprite.dragSpeed = data[item]["drag"] sprite.xFrq = data[item]["xFrq"] sprite.xAmp = data[item]["xAmp"] sprite.yFrq = data[item]["yFrq"] sprite.yAmp = data[item]["yAmp"] # Load rotation wobble properties (with backward compatibility) if data[item].has("rFrq"): sprite.rFrq = data[item]["rFrq"] if data[item].has("rAmp"): sprite.rAmp = data[item]["rAmp"] sprite.rdragStr = data[item]["rotDrag"] sprite.showOnTalk = data[item]["showTalk"] sprite.showOnBlink = data[item]["showBlink"] if data[item].has("rLimitMin"): sprite.rLimitMin = data[item]["rLimitMin"] if data[item].has("rLimitMax"): sprite.rLimitMax = data[item]["rLimitMax"] if data[item].has("costumeLayers"): sprite.costumeLayers = str_to_var(data[item]["costumeLayers"]).duplicate() # Ensure we have exactly 10 costume layers while sprite.costumeLayers.size() < 10: sprite.costumeLayers.append(1) if data[item].has("stretchAmount"): sprite.stretchAmount = data[item]["stretchAmount"] if data[item].has("ignoreBounce"): sprite.ignoreBounce = data[item]["ignoreBounce"] if data[item].has("frames"): sprite.frames = data[item]["frames"] if data[item].has("animSpeed"): sprite.animSpeed = data[item]["animSpeed"] if data[item].has("imageData"): sprite.loadedImageData = data[item]["imageData"] if data[item].has("clipped"): sprite.clipped = data[item]["clipped"] if data[item].has("toggle"): sprite.toggle = data[item]["toggle"] if data[item].has("spriteOpacity"): sprite.spriteOpacity = data[item]["spriteOpacity"] if data[item].has("affectChildrenOpacity"): sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"] if data[item].has("blendMode"): sprite.blendMode = data[item]["blendMode"] # Load wobble sync group (with backward compatibility) if data[item].has("wobbleSyncGroup"): sprite.wobbleSyncGroup = data[item]["wobbleSyncGroup"] # Load transform properties (with backward compatibility) if data[item].has("staticRotation"): sprite.staticRotation = data[item]["staticRotation"] if data[item].has("mirrorHorizontal"): sprite.mirrorHorizontal = data[item]["mirrorHorizontal"] if data[item].has("mirrorVertical"): sprite.mirrorVertical = data[item]["mirrorVertical"] origin.add_child(sprite) sprite.position = str_to_var(data[item]["pos"]) changeCostume(1) Saving.settings["lastAvatar"] = path Global.spriteList.updateData() # Load wobble sync groups if data.has("wobbleSyncGroups") and WobbleSyncManager: await WobbleSyncManager.loadSaveData(data["wobbleSyncGroups"]) # Refresh wobble sync UI after groups are loaded if Global.spriteEdit and Global.spriteEdit.wobbleSyncControl: Global.spriteEdit.wobbleSyncControl.updateUI() # Force refresh in case timing issues await get_tree().process_frame Global.spriteEdit.wobbleSyncControl.forceRefresh() # Force refresh in case timing issues await get_tree().process_frame Global.spriteEdit.wobbleSyncControl.forceRefresh() # Update opacity shaders for all loaded sprites after wobble sync data is applied await get_tree().process_frame # Wait for all sprites to be fully initialized var allSprites = get_tree().get_nodes_in_group("saved") for sprite in allSprites: if sprite.has_method("updateOpacity"): sprite.updateOpacity() Global.pushUpdate("Loaded avatar at: " + path) onWindowSizeChange() ## Load the default avatar data func loadDefaultAvatar(): _on_load_dialog_file_selected("default") ## Load avatar from a specific file path func loadAvatar(path: String): _on_load_dialog_file_selected(path) #SAVE AVATAR func _on_save_dialog_file_selected(path): var data = {} var nodes = get_tree().get_nodes_in_group("saved") var id = 0 for child in nodes: if child.type == "sprite": data[id] = {} data[id]["type"] = "sprite" data[id]["path"] = child.path data[id]["displayName"] = child.displayName data[id]["imageData"] = Marshalls.raw_to_base64(child.imageData.save_png_to_buffer()) data[id]["identification"] = child.id data[id]["parentId"] = child.parentId data[id]["pos"] = var_to_str(child.position) data[id]["offset"] = var_to_str(child.offset) data[id]["zindex"] = child.z data[id]["drag"] = child.dragSpeed data[id]["xFrq"] = child.xFrq data[id]["xAmp"] = child.xAmp data[id]["yFrq"] = child.yFrq data[id]["yAmp"] = child.yAmp data[id]["rFrq"] = child.rFrq data[id]["rAmp"] = child.rAmp data[id]["rotDrag"] = child.rdragStr data[id]["showTalk"] = child.showOnTalk data[id]["showBlink"] = child.showOnBlink data[id]["rLimitMin"] = child.rLimitMin data[id]["rLimitMax"] = child.rLimitMax data[id]["costumeLayers"] = var_to_str(child.costumeLayers) data[id]["stretchAmount"] = child.stretchAmount data[id]["ignoreBounce"] = child.ignoreBounce data[id]["frames"] = child.frames data[id]["animSpeed"] = child.animSpeed data[id]["clipped"] = child.clipped data[id]["toggle"] = child.toggle data[id]["spriteOpacity"] = child.spriteOpacity data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity data[id]["blendMode"] = child.blendMode # Save wobble sync group data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup # Save transform properties data[id]["staticRotation"] = child.staticRotation data[id]["mirrorHorizontal"] = child.mirrorHorizontal data[id]["mirrorVertical"] = child.mirrorVertical id += 1 # Save wobble sync groups if WobbleSyncManager: data["wobbleSyncGroups"] = WobbleSyncManager.getSaveData() Saving.settings["lastAvatar"] = path Saving.data = data.duplicate() Saving.write_save(path) Global.pushUpdate("Saved avatar at: " + path) func _on_link_button_pressed(): Global.reparentMode = true Global.chain.enable(Global.reparentMode) Global.pushUpdate("Linking sprite...") func _on_kofi_pressed(): OS.shell_open("https://ko-fi.com/kaiakairos") Global.pushUpdate("Support me on ko-fi!") func _on_bluesky_pressed(): OS.shell_open("https://bsky.app/profile/kaiakairos.net") Global.pushUpdate("Follow me on bluesky!") func _on_replace_button_pressed(): if Global.heldSprite == null: return $ReplaceDialog.visible = true func _on_replace_dialog_file_selected(path): Global.heldSprite.replaceSprite(path) Global.spriteList.updateData() Global.pushUpdate("Replacing sprite with: " + path) func _on_replace_dialog_visibility_changed(): $EditControls/ScreenCover/CollisionShape2D.disabled = !$ReplaceDialog.visible func _on_duplicate_button_pressed(): if Global.heldSprite == null: return var rand = RandomNumberGenerator.new() var id = rand.randi() var sprite = spriteObject.instantiate() sprite.path = Global.heldSprite.path sprite.id = id sprite.parentId = Global.heldSprite.parentId sprite.dragSpeed = Global.heldSprite.dragSpeed sprite.showOnTalk = Global.heldSprite.showOnTalk sprite.showOnBlink = Global.heldSprite.showOnBlink sprite.z = Global.heldSprite.z sprite.xFrq = Global.heldSprite.xFrq sprite.xAmp = Global.heldSprite.xAmp sprite.yFrq = Global.heldSprite.yFrq sprite.yAmp = Global.heldSprite.yAmp sprite.rFrq = Global.heldSprite.rFrq sprite.rAmp = Global.heldSprite.rAmp sprite.rdragStr = Global.heldSprite.rdragStr sprite.offset = Global.heldSprite.offset sprite.rLimitMin = Global.heldSprite.rLimitMin sprite.rLimitMax = Global.heldSprite.rLimitMax sprite.frames = Global.heldSprite.frames sprite.animSpeed = Global.heldSprite.animSpeed sprite.costumeLayers = Global.heldSprite.costumeLayers sprite.spriteOpacity = Global.heldSprite.spriteOpacity sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity sprite.blendMode = Global.heldSprite.blendMode # Copy wobble sync group sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup # Copy transform properties sprite.staticRotation = Global.heldSprite.staticRotation sprite.mirrorHorizontal = Global.heldSprite.mirrorHorizontal sprite.mirrorVertical = Global.heldSprite.mirrorVertical origin.add_child(sprite) sprite.position = Global.heldSprite.position + Vector2(16,16) # Update opacity shader for the duplicated sprite await get_tree().process_frame # Wait for sprite to be fully initialized sprite.updateOpacity() Global.heldSprite = sprite Global.spriteList.updateData() Global.pushUpdate("Duplicated sprite.") func changeCostumeStreamDeck(id: String): match id: "1":changeCostume(1) "2":changeCostume(2) "3":changeCostume(3) "4":changeCostume(4) "5":changeCostume(5) "6":changeCostume(6) "7":changeCostume(7) "8":changeCostume(8) "9":changeCostume(9) "10":changeCostume(10) func changeCostume(newCostume, preserve_selection: bool = false): costume = newCostume # Only clear heldSprite for actual costume changes, not layer updates if not preserve_selection: Global.heldSprite = null var nodes = get_tree().get_nodes_in_group("saved") for sprite in nodes: if sprite.costumeLayers[newCostume-1] == 1: sprite.visible = true sprite.changeCollision(true) else: sprite.visible = false sprite.changeCollision(false) Global.spriteEdit.layerSelected() spriteList.updateAllVisible() if bounceOnCostumeChange: onSpeak() Global.pushUpdate("Change costume: " + str(newCostume)) func moveSpriteMenu(delta): #moves sprite viewer editor thing around var size = get_viewport().get_visible_rect().size var windowLength = 2000 $ViewerArrows/Arrows.position.y = size.y - 25 if !Global.spriteEdit.visible: $ViewerArrows/Arrows.visible = false $ViewerArrows/Arrows2.visible = false return if size.y > windowLength+50: Global.spriteEdit.position.y = 66 $ViewerArrows/Arrows.visible = false $ViewerArrows/Arrows2.visible = false return if Global.spriteEdit.position.y < 16: $ViewerArrows/Arrows2.visible = true else: $ViewerArrows/Arrows2.visible = false if Global.spriteEdit.position.y > size.y-windowLength+2: $ViewerArrows/Arrows.visible = true else: $ViewerArrows/Arrows.visible = false if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area): Global.spriteEdit.position.y += (delta * 400.0) elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area): Global.spriteEdit.position.y -= (delta * 400.0) if Global.spriteEdit.position.y > 66: Global.spriteEdit.position.y = 66 elif Global.spriteEdit.position.y < size.y-windowLength: Global.spriteEdit.position.y = size.y-windowLength #UNAMED BUT THIS IS THE MICROPHONE MENU BUTTON func _on_button_pressed(): $ControlPanel/MicInputSelect.visible = !$ControlPanel/MicInputSelect.visible settingsMenu.visible = false func _on_settings_buttons_pressed(): settingsMenu.visible = !settingsMenu.visible func _on_background_input_capture_bg_key_pressed(node, keys_pressed): var keyStrings = [] # Check for modifier keys first (cross-platform) var modifiers = [] var has_ctrl = keys_pressed.has(KEY_CTRL) and keys_pressed[KEY_CTRL] var has_shift = keys_pressed.has(KEY_SHIFT) and keys_pressed[KEY_SHIFT] var has_alt = keys_pressed.has(KEY_ALT) and keys_pressed[KEY_ALT] var has_meta = keys_pressed.has(KEY_META) and keys_pressed[KEY_META] # Command key on macOS # Build modifier prefix once if has_ctrl: modifiers.append("Ctrl") if has_shift: modifiers.append("Shift") if has_alt: modifiers.append("Alt") if has_meta: modifiers.append("Cmd") # Command key on macOS var modifier_prefix = "" if modifiers.size() > 0: modifier_prefix = "+".join(modifiers) + "+" # Single pass: build key strings directly for i in keys_pressed: if keys_pressed[i]: # Skip modifier keys if i == KEY_CTRL or i == KEY_SHIFT or i == KEY_ALT or i == KEY_META: continue # Skip mouse button keycodes (common source of phantom inputs) if i == MOUSE_BUTTON_LEFT or i == MOUSE_BUTTON_RIGHT or i == MOUSE_BUTTON_MIDDLE: continue # Skip phantom keypad inputs if no actual keypad connected var key_name = OS.get_keycode_string(i) if key_name.begins_with("kp ") or key_name.begins_with("Kp "): # Skip keypad inputs unless they're from actual key presses continue if !key_name.strip_edges().is_empty(): keyStrings.append(modifier_prefix + key_name) else: keyStrings.append(modifier_prefix + "Keycode" + str(i)) if fileSystemOpen: return if keyStrings.size() <= 0: emit_signal("emptiedCapture") emit_signal("fatfuckingballs") return if settingsMenu.awaitingCostumeInput >= 0: # Cancel key assignment if user clicks outside the settings menu if keyStrings[0] == "Keycode1" and !settingsMenu.hasMouse: # Don't assign the key, just complete the flow emit_signal("pressedKey") return var currentButton = costumeKeys[settingsMenu.awaitingCostumeInput] costumeKeys[settingsMenu.awaitingCostumeInput] = keyStrings[0] Saving.settings["costumeKeys"] = costumeKeys Global.pushUpdate("Changed costume " + str(settingsMenu.awaitingCostumeInput+1) + " hotkey from \"" + currentButton + "\" to \"" + keyStrings[0] + "\"") emit_signal("pressedKey") return # Handle sprite visibility toggles if not waiting for costume input spriteVisToggles.emit(keyStrings) for key in keyStrings: var i = costumeKeys.find(key) if i >= 0: changeCostume(i+1) ## Simple NDI control function func toggle_ndi(): if ndi_output: print("NDI Output is available - Source: ", ndi_output.name) print("NDI should be streaming the main viewport") else: print("NDI Output not found") ## Set NDI source name func set_ndi_source_name(source_name: String): if ndi_output: ndi_output.name = source_name print("NDI source name set to: ", source_name)