diff --git a/README.md b/README.md index 3be3c9b..eb706f5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,17 @@ Sprites can now do a subtle (or chaotic) rotational wobble. Frequency + amplitud ![Rotation Wobble](https://github.com/user-attachments/assets/f45c3cc8-98d8-48cf-8821-b015de0a2f78) +### 🔗 Wobble Sync Groups + +Keep multiple sprites in sync with wobble sync groups. Create groups, assign sprites, and all wobble parameters (X/Y/Rotation frequency, amplitude) automatically sync across group members. Perfect for ears, hair, accessories, or any elements that should move together. + +* Create custom sync groups with descriptive names +* Assign multiple sprites to the same group +* Edit any group member's wobble settings to update the entire group +* Visual indicators show which sprites are synced +* Groups persist in save files + + ### ⌨️ Enhanced Modifier Keys Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fingers. diff --git a/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll b/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll new file mode 100644 index 0000000..60e5fe6 Binary files /dev/null and b/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll differ diff --git a/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll~RF1a2259cc.TMP b/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll~RF1a2259cc.TMP new file mode 100644 index 0000000..60e5fe6 Binary files /dev/null and b/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll~RF1a2259cc.TMP differ diff --git a/autoload/wobble_sync_manager.gd b/autoload/wobble_sync_manager.gd new file mode 100644 index 0000000..3ca917e --- /dev/null +++ b/autoload/wobble_sync_manager.gd @@ -0,0 +1,452 @@ +extends Node + +## WobbleSyncGroupManager - Manages synchronized wobble groups for sprites +## Follows class-based OOP pattern as requested + +var syncGroups: Dictionary = {} # groupName: WobbleSyncGroup +var spriteGroupMembership: Dictionary = {} # spriteId: groupName + +signal group_created(group_name: String) +signal group_deleted(group_name: String) +signal sprite_joined_group(sprite_id: int, group_name: String) +signal sprite_left_group(sprite_id: int, group_name: String) + +func _ready(): + print("DEBUG: WobbleSyncManager._ready() called") + print("DEBUG: WobbleSyncManager autoload initialized successfully") + print("DEBUG: Initial syncGroups: ", syncGroups) + print("DEBUG: Initial spriteGroupMembership: ", spriteGroupMembership) + +class WobbleSyncGroup: + var name: String + var xFrq: float = 0.0 + var xAmp: float = 0.0 + var yFrq: float = 0.0 + var yAmp: float = 0.0 + var rFrq: float = 0.0 + var rAmp: float = 0.0 + var memberSprites: Array = [] + + func _init(group_name: String): + name = group_name + + ## Set wobble parameters for the entire group + func setWobbleParameters(x_frq: float, x_amp: float, y_frq: float, y_amp: float, r_frq: float, r_amp: float, manager_ref): + print("DEBUG: WobbleSyncGroup.setWobbleParameters called for group '", name, "' with ", memberSprites.size(), " members") + print("DEBUG: Member sprite IDs: ", memberSprites) + xFrq = x_frq + xAmp = x_amp + yFrq = y_frq + yAmp = y_amp + rFrq = r_frq + rAmp = r_amp + + # Update all member sprites using the manager reference + var sprites = manager_ref.get_tree().get_nodes_in_group("saved") + print("DEBUG: Found ", sprites.size(), " total sprites in 'saved' group") + + var updated_count = 0 + for sprite in sprites: + print("DEBUG: Checking sprite ID ", sprite.id, " (type: ", typeof(sprite.id), ") against member list") + if sprite.id in memberSprites: + print("DEBUG: Updating sprite ID ", sprite.id, " with new wobble values (no scaling - values stored as-is)") + # Preserve the sprite's group membership + var current_group = sprite.wobbleSyncGroup + sprite.xFrq = xFrq + sprite.xAmp = xAmp + sprite.yFrq = yFrq + sprite.yAmp = yAmp + sprite.rFrq = rFrq + sprite.rAmp = rAmp + # Restore group membership in case it got cleared + sprite.wobbleSyncGroup = current_group + updated_count += 1 + else: + print("DEBUG: Sprite ID ", sprite.id, " not in member list") + + print("DEBUG: Updated ", updated_count, " sprites in group '", name, "'") + + ## Add sprite to this group + func addSprite(sprite_id): # Remove type constraint to handle float IDs + print("DEBUG: WobbleSyncGroup.addSprite called with sprite_id: ", sprite_id, " (type: ", typeof(sprite_id), ")") + if sprite_id not in memberSprites: + memberSprites.append(sprite_id) + print("DEBUG: Added sprite to memberSprites. Current members: ", memberSprites) + else: + print("DEBUG: Sprite already in memberSprites") + + ## Remove sprite from this group + func removeSprite(sprite_id): # Remove type constraint + memberSprites.erase(sprite_id) + + ## Check if group is empty + func isEmpty() -> bool: + return memberSprites.size() == 0 + + ## Get group data for saving + func toSaveData() -> Dictionary: + return { + "name": name, + "xFrq": xFrq, + "xAmp": xAmp, + "yFrq": yFrq, + "yAmp": yAmp, + "rFrq": rFrq, + "rAmp": rAmp, + "memberSprites": memberSprites + } + + ## Load group data from save + func fromSaveData(data: Dictionary): + name = data.get("name", "") + xFrq = data.get("xFrq", 0.0) + xAmp = data.get("xAmp", 0.0) + yFrq = data.get("yFrq", 0.0) + yAmp = data.get("yAmp", 0.0) + rFrq = data.get("rFrq", 0.0) + rAmp = data.get("rAmp", 0.0) + memberSprites = data.get("memberSprites", []) + +## Create a new wobble sync group +func createGroup(group_name: String, sprite_id = -1) -> bool: # Remove type constraint + if group_name.strip_edges() == "" or syncGroups.has(group_name): + return false + + var group = WobbleSyncGroup.new(group_name) + syncGroups[group_name] = group + + # If a sprite ID is provided, add it and copy its wobble values + if sprite_id != -1: + var sprite = getSpriteById(sprite_id) + if sprite: + # Store the sprite's raw wobble values directly + group.setWobbleParameters(sprite.xFrq, sprite.xAmp, sprite.yFrq, sprite.yAmp, sprite.rFrq, sprite.rAmp, self) + addSpriteToGroup(sprite_id, group_name) + + group_created.emit(group_name) + Global.pushUpdate("Created wobble sync group: " + group_name) + return true + +## Delete a wobble sync group +func deleteGroup(group_name: String): + if not syncGroups.has(group_name): + return + + var group = syncGroups[group_name] + + # Remove all sprites from the group + for sprite_id in group.memberSprites.duplicate(): + removeSpriteFromGroup(sprite_id, group_name) + + syncGroups.erase(group_name) + group_deleted.emit(group_name) + Global.pushUpdate("Deleted wobble sync group: " + group_name) + +## Add sprite to a group +func addSpriteToGroup(sprite_id, group_name: String) -> bool: # Remove type constraint + print("DEBUG: addSpriteToGroup called with sprite_id: ", sprite_id, " (type: ", typeof(sprite_id), "), group: '", group_name, "'") + + if not syncGroups.has(group_name): + print("DEBUG: Group '", group_name, "' not found") + return false + + # Remove sprite from any existing group first + removeSpriteFromAllGroups(sprite_id) + + var group = syncGroups[group_name] + group.addSprite(sprite_id) + spriteGroupMembership[sprite_id] = group_name + print("DEBUG: Added sprite ", sprite_id, " to group '", group_name, "'. Group now has ", group.memberSprites.size(), " members") + + # Sync the sprite's wobble parameters to the group + var sprite = getSpriteById(sprite_id) + if sprite: + print("DEBUG: Found sprite object, syncing wobble parameters to group values") + sprite.wobbleSyncGroup = group_name + sprite.xFrq = group.xFrq + sprite.xAmp = group.xAmp + sprite.yFrq = group.yFrq + sprite.yAmp = group.yAmp + sprite.rFrq = group.rFrq + sprite.rAmp = group.rAmp + + sprite_joined_group.emit(sprite_id, group_name) + Global.pushUpdate("Added sprite to wobble sync group: " + group_name) + return true + +## Remove sprite from a specific group +func removeSpriteFromGroup(sprite_id, group_name: String): # Remove type constraint + print("DEBUG: removeSpriteFromGroup called for sprite ID: ", sprite_id, " (type: ", typeof(sprite_id), "), group: '", group_name, "'") + if not syncGroups.has(group_name): + print("DEBUG: Group '", group_name, "' not found") + return + + var group = syncGroups[group_name] + print("DEBUG: Group before removal has ", group.memberSprites.size(), " members: ", group.memberSprites) + group.removeSprite(sprite_id) + spriteGroupMembership.erase(sprite_id) + print("DEBUG: Group after removal has ", group.memberSprites.size(), " members: ", group.memberSprites) + + # Clear the sprite's sync group reference + var sprite = getSpriteById(sprite_id) + if sprite: + print("DEBUG: Clearing sprite's wobbleSyncGroup property") + sprite.wobbleSyncGroup = "" + else: + print("DEBUG: Could not find sprite object for ID: ", sprite_id) + + sprite_left_group.emit(sprite_id, group_name) + + # Auto-delete empty groups + if group.isEmpty(): + deleteGroup(group_name) + else: + Global.pushUpdate("Removed sprite from wobble sync group: " + group_name) + +## Remove sprite from all groups +func removeSpriteFromAllGroups(sprite_id): # Remove type constraint + print("DEBUG: removeSpriteFromAllGroups called for sprite ID: ", sprite_id) + if spriteGroupMembership.has(sprite_id): + var group_name = spriteGroupMembership[sprite_id] + print("DEBUG: Found sprite in group '", group_name, "', removing...") + removeSpriteFromGroup(sprite_id, group_name) + else: + print("DEBUG: Sprite not found in any group membership") + +## Update wobble parameters for a group +func updateGroupWobble(group_name: String, x_frq: float, x_amp: float, y_frq: float, y_amp: float, r_frq: float, r_amp: float): + print("DEBUG: updateGroupWobble called for group '", group_name, "' with values - xFrq:", x_frq, " xAmp:", x_amp, " yFrq:", y_frq, " yAmp:", y_amp, " rFrq:", r_frq, " rAmp:", r_amp) + + if not syncGroups.has(group_name): + print("DEBUG: Group '", group_name, "' not found in syncGroups!") + print("DEBUG: Available groups: ", syncGroups.keys()) + print("DEBUG: This suggests the avatar's wobble sync groups were never loaded from the save file!") + print("DEBUG: Either the save file doesn't contain wobbleSyncGroups, or the loading process didn't run.") + return + + var group = syncGroups[group_name] + print("DEBUG: Found group, calling setWobbleParameters on group with ", group.memberSprites.size(), " members") + group.setWobbleParameters(x_frq, x_amp, y_frq, y_amp, r_frq, r_amp, self) + +## Get sprite's current group name +func getSpriteGroupName(sprite_id: int) -> String: + return spriteGroupMembership.get(sprite_id, "") + +## Get all group names +func getGroupNames() -> Array: + print("DEBUG: getGroupNames called. syncGroups has ", syncGroups.size(), " groups: ", syncGroups.keys()) + print("DEBUG: getGroupNames - syncGroups reference: ", syncGroups) + print("DEBUG: getGroupNames - self reference: ", self) + return syncGroups.keys() + +## Test function to manually load wobble sync groups +func testManualLoad(): + print("DEBUG: testManualLoad called - creating test data...") + var test_data = { + "ears": { + "name": "ears", + "xFrq": 0.0, + "xAmp": 0.0, + "yFrq": 0.019, + "yAmp": 3.0, + "rFrq": 0.399, + "rAmp": 47.0, + "memberSprites": [3291286625.0] + } + } + print("DEBUG: Calling loadSaveData with test data...") + await loadSaveData(test_data) + print("DEBUG: testManualLoad completed. syncGroups now has: ", syncGroups.size(), " groups") + +## Check if a group exists +func hasGroup(group_name: String) -> bool: + return syncGroups.has(group_name) + +## Get sprite by ID +func getSpriteById(sprite_id): # Remove type constraint to handle float IDs + var sprites = get_tree().get_nodes_in_group("saved") + for sprite in sprites: + if sprite.id == sprite_id: + return sprite + return null + +## Clean up orphaned sprites (sprites that no longer exist) +func cleanupOrphanedSprites(): + var existing_sprite_ids = [] + var sprites = get_tree().get_nodes_in_group("saved") + for sprite in sprites: + existing_sprite_ids.append(sprite.id) + + # Check each group for orphaned sprite IDs + for group_name in syncGroups.keys(): + var group = syncGroups[group_name] + var orphaned_ids = [] + + for sprite_id in group.memberSprites: + if sprite_id not in existing_sprite_ids: + orphaned_ids.append(sprite_id) + + # Remove orphaned IDs + for orphaned_id in orphaned_ids: + group.removeSprite(orphaned_id) + spriteGroupMembership.erase(orphaned_id) + + # Delete empty groups + if group.isEmpty(): + deleteGroup(group_name) + +## Get save data for all groups +func getSaveData() -> Dictionary: + var save_data = {} + for group_name in syncGroups.keys(): + var group = syncGroups[group_name] + save_data[group_name] = group.toSaveData() + return save_data + +## Load save data for all groups +func loadSaveData(save_data: Dictionary): + print("DEBUG: ========================") + print("DEBUG: WobbleSyncManager.loadSaveData called with save_data: ", save_data) + print("DEBUG: save_data type: ", typeof(save_data)) + print("DEBUG: save_data.keys(): ", save_data.keys()) + print("DEBUG: save_data.size(): ", save_data.size()) + print("DEBUG: self reference: ", self) + print("DEBUG: syncGroups before clear: ", syncGroups) + + # Check if save_data is valid + if save_data == null or typeof(save_data) != TYPE_DICTIONARY: + print("DEBUG: ERROR - Invalid save_data provided") + return + + if save_data.size() == 0: + print("DEBUG: WARNING - Empty save_data provided") + return + + # Clear existing groups + print("DEBUG: Clearing existing syncGroups (had ", syncGroups.size(), " groups)") + syncGroups.clear() + spriteGroupMembership.clear() + print("DEBUG: Cleared existing data. syncGroups now has ", syncGroups.size(), " groups") + + # Load groups from save data + var loaded_groups = 0 + print("DEBUG: Starting group loading loop...") + for group_name in save_data.keys(): + print("DEBUG: ---- Processing group '", group_name, "' (attempt ", loaded_groups + 1, ") ----") + var group_data = save_data[group_name] + print("DEBUG: Group data type: ", typeof(group_data)) + print("DEBUG: Group data: ", group_data) + + if typeof(group_data) == TYPE_DICTIONARY: + print("DEBUG: Creating WobbleSyncGroup instance...") + var group = WobbleSyncGroup.new(group_name) + print("DEBUG: Created new WobbleSyncGroup instance for '", group_name, "'") + print("DEBUG: Calling fromSaveData...") + group.fromSaveData(group_data) + print("DEBUG: Called fromSaveData on group '", group_name, "'") + print("DEBUG: Adding to syncGroups dictionary...") + syncGroups[group_name] = group + loaded_groups += 1 + print("DEBUG: Added group '", group_name, "' to syncGroups. Total groups now: ", syncGroups.size()) + print("DEBUG: syncGroups.keys() now: ", syncGroups.keys()) + print("DEBUG: Group memberSprites: ", group.memberSprites) + + # Update sprite group membership + print("DEBUG: Updating sprite group membership...") + for sprite_id in group.memberSprites: + spriteGroupMembership[sprite_id] = group_name + print("DEBUG: Added sprite ", sprite_id, " to spriteGroupMembership for group '", group_name, "'") + print("DEBUG: ---- Finished processing group '", group_name, "' ----") + else: + print("DEBUG: ERROR - Group data for '", group_name, "' is not a Dictionary, skipping") + + print("DEBUG: ========================") + print("DEBUG: GROUP LOADING COMPLETE") + print("DEBUG: Loaded ", loaded_groups, " groups total") + print("DEBUG: Final syncGroups after loading: ", syncGroups.keys()) + print("DEBUG: Final syncGroups size: ", syncGroups.size()) + print("DEBUG: Final syncGroups contents: ", syncGroups) + print("DEBUG: Final spriteGroupMembership: ", spriteGroupMembership) + print("DEBUG: ========================") + + # If no groups were loaded, clear any orphaned group references on sprites + if syncGroups.size() == 0: + print("DEBUG: No groups loaded, clearing orphaned sprite group references") + var sprites = get_tree().get_nodes_in_group("saved") + for sprite in sprites: + if sprite.wobbleSyncGroup != "": + print("DEBUG: Clearing orphaned group reference '", sprite.wobbleSyncGroup, "' from sprite ID ", sprite.id) + sprite.wobbleSyncGroup = "" + else: + print("DEBUG: Groups loaded successfully, syncing sprite parameters...") + # Apply wobble parameters to sprites with better timing + await get_tree().process_frame # Wait for sprites to be loaded + await get_tree().process_frame # Extra frame to ensure everything is ready + + for group_name in syncGroups.keys(): + var group = syncGroups[group_name] + print("DEBUG: Syncing parameters for group '", group_name, "' with ", group.memberSprites.size(), " members") + for sprite_id in group.memberSprites: + var sprite = getSpriteById(sprite_id) + if sprite: + print("DEBUG: Syncing sprite ID ", sprite_id, " to group '", group_name, "'") + sprite.wobbleSyncGroup = group_name + sprite.xFrq = group.xFrq + sprite.xAmp = group.xAmp + sprite.yFrq = group.yFrq + sprite.yAmp = group.yAmp + sprite.rFrq = group.rFrq + sprite.rAmp = group.rAmp + # Force a complete opacity update after modifying sprite properties + if sprite.has_method("updateOpacity"): + sprite.updateOpacity() + if sprite.has_method("updateShaderOpacity"): + sprite.updateShaderOpacity() + else: + print("DEBUG: ERROR - Could not find sprite with ID ", sprite_id) + + print("DEBUG: ========================") + print("DEBUG: loadSaveData FUNCTION COMPLETE") + print("DEBUG: Final verification - syncGroups has ", syncGroups.size(), " groups: ", syncGroups.keys()) + print("DEBUG: ========================") + +## Called when a sprite is deleted to clean up references +func onSpriteDeleted(sprite_id): # Remove type constraint + removeSpriteFromAllGroups(sprite_id) + +## Debug function to check data consistency +func checkDataConsistency(): + print("=== WOBBLE SYNC DATA CONSISTENCY CHECK ===") + + # Check each group's member sprites + for group_name in syncGroups.keys(): + var group = syncGroups[group_name] + print("Group '", group_name, "' has ", group.memberSprites.size(), " members: ", group.memberSprites) + + for sprite_id in group.memberSprites: + # Check if sprite exists + var sprite = getSpriteById(sprite_id) + if sprite == null: + print(" ERROR: Sprite ID ", sprite_id, " in group but sprite object not found!") + else: + # Check if sprite's wobbleSyncGroup matches + if sprite.wobbleSyncGroup != group_name: + print(" ERROR: Sprite ID ", sprite_id, " in group '", group_name, "' but sprite.wobbleSyncGroup = '", sprite.wobbleSyncGroup, "'") + else: + print(" OK: Sprite ID ", sprite_id, " consistent") + + # Check if membership mapping is correct + if not spriteGroupMembership.has(sprite_id) or spriteGroupMembership[sprite_id] != group_name: + print(" ERROR: Sprite ID ", sprite_id, " in group but spriteGroupMembership is wrong!") + + # Check membership mapping for orphaned entries + for sprite_id in spriteGroupMembership.keys(): + var group_name = spriteGroupMembership[sprite_id] + if not syncGroups.has(group_name): + print("ERROR: Sprite ID ", sprite_id, " mapped to non-existent group '", group_name, "'") + else: + var group = syncGroups[group_name] + if sprite_id not in group.memberSprites: + print("ERROR: Sprite ID ", sprite_id, " mapped to group '", group_name, "' but not in group's member list") + + print("=== END CONSISTENCY CHECK ===") diff --git a/autoload/wobble_sync_manager.gd.uid b/autoload/wobble_sync_manager.gd.uid new file mode 100644 index 0000000..41f5ca1 --- /dev/null +++ b/autoload/wobble_sync_manager.gd.uid @@ -0,0 +1 @@ +uid://ccl0r2eov68sy diff --git a/bin/~libgdexample.windows.template_debug.x86_64.dll b/bin/~libgdexample.windows.template_debug.x86_64.dll new file mode 100644 index 0000000..c28b9ad Binary files /dev/null and b/bin/~libgdexample.windows.template_debug.x86_64.dll differ diff --git a/bin/~libgdexample.windows.template_debug.x86_64.dll~RF1a2259db.TMP b/bin/~libgdexample.windows.template_debug.x86_64.dll~RF1a2259db.TMP new file mode 100644 index 0000000..c28b9ad Binary files /dev/null and b/bin/~libgdexample.windows.template_debug.x86_64.dll~RF1a2259db.TMP differ diff --git a/main_scenes/main.gd b/main_scenes/main.gd index d8a0715..5ca0cda 100644 --- a/main_scenes/main.gd +++ b/main_scenes/main.gd @@ -78,14 +78,27 @@ func _ready(): Global.connect("startSpeaking",onSpeak) - ElgatoStreamDeck.on_key_down.connect(changeCostumeStreamDeck) + # 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 Saving.settings["newUser"]: + print("DEBUG: MAIN._ready() - New user detected, loading default avatar") _on_load_dialog_file_selected("default") Saving.settings["newUser"] = false saveLoaded = true else: - _on_load_dialog_file_selected(Saving.settings["lastAvatar"]) + print("DEBUG: MAIN._ready() - Existing user, loading last avatar") + var lastAvatar = Saving.settings["lastAvatar"] + print("DEBUG: lastAvatar setting: ", lastAvatar, " (type: ", typeof(lastAvatar), ")") + # Ensure lastAvatar is a string, not a Dictionary + if typeof(lastAvatar) == TYPE_STRING and lastAvatar != "": + print("DEBUG: Loading avatar from: ", lastAvatar) + _on_load_dialog_file_selected(lastAvatar) + else: + print("WARNING: lastAvatar setting is invalid, loading default instead") + _on_load_dialog_file_selected("default") $ControlPanel/volumeSlider.value = Saving.settings["volume"] $ControlPanel/sensitiveSlider.value = Saving.settings["sense"] @@ -245,6 +258,11 @@ func isFileSystemOpen(): 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 @@ -407,17 +425,52 @@ func _on_load_button_pressed(): #LOAD AVATAR func _on_load_dialog_file_selected(path): + print("DEBUG: _on_load_dialog_file_selected called with path: ", path, " (type: ", typeof(path), ")") + + # Handle case where path might be a Dictionary instead of string + if typeof(path) == TYPE_DICTIONARY: + print("ERROR: Expected string path but got Dictionary: ", path) + return + + print("DEBUG: About to read save data from: ", path) var data = Saving.read_save(path) + print("DEBUG: Save data read. Data type: ", typeof(data), ", null check: ", data == null) if data == null: + print("DEBUG: Save data is null, returning") return + print("DEBUG: Save data keys: ", data.keys() if typeof(data) == TYPE_DICTIONARY else "not a dictionary") + print("DEBUG: Save data size: ", data.size() if typeof(data) == TYPE_DICTIONARY else "not a dictionary") + + # Check the structure of the first few items + if typeof(data) == TYPE_DICTIONARY: + var count = 0 + for key in data.keys(): + if count < 3: # Just show first 3 items for debugging + print("DEBUG: data[", key, "] = ", data[key]) + print("DEBUG: data[", key, "] type = ", typeof(data[key])) + if typeof(data[key]) == TYPE_DICTIONARY and data[key].has("path"): + print("DEBUG: data[", key, "][\"path\"] = ", data[key]["path"]) + else: + print("DEBUG: data[", key, "] doesn't have 'path' key or isn't a dictionary") + count += 1 + origin.queue_free() var new = Node2D.new() $OriginMotion.add_child(new) origin = new for item in data: + print("DEBUG: Processing item: ", item, ", data type: ", typeof(data[item])) + if typeof(data[item]) != TYPE_DICTIONARY: + print("DEBUG: Skipping item ", item, " - not a dictionary") + continue + + if not data[item].has("path"): + print("DEBUG: Skipping item ", item, " - no 'path' key. Keys: ", data[item].keys()) + continue + var sprite = spriteObject.instantiate() sprite.path = data[item]["path"] sprite.id = data[item]["identification"] @@ -475,6 +528,10 @@ func _on_load_dialog_file_selected(path): if data[item].has("affectChildrenOpacity"): sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"] + # Load wobble sync group (with backward compatibility) + if data[item].has("wobbleSyncGroup"): + sprite.wobbleSyncGroup = data[item]["wobbleSyncGroup"] + origin.add_child(sprite) sprite.position = str_to_var(data[item]["pos"]) @@ -482,7 +539,43 @@ func _on_load_dialog_file_selected(path): Saving.settings["lastAvatar"] = path Global.spriteList.updateData() - # Update opacity shaders for all loaded sprites after they're all created + # Load wobble sync groups + print("DEBUG: About to check for wobble sync groups...") + print("DEBUG: data type: ", typeof(data), ", data.keys(): ", data.keys() if typeof(data) == TYPE_DICTIONARY else "not a dictionary") + print("DEBUG: data.has('wobbleSyncGroups'): ", data.has("wobbleSyncGroups")) + print("DEBUG: WobbleSyncManager exists: ", WobbleSyncManager != null) + if WobbleSyncManager != null: + print("DEBUG: WobbleSyncManager type: ", typeof(WobbleSyncManager)) + + # Show save data content related to wobble sync + if data.has("wobbleSyncGroups"): + print("DEBUG: wobbleSyncGroups data found: ", data["wobbleSyncGroups"]) + else: + print("DEBUG: No wobbleSyncGroups found in save data") + + if data.has("wobbleSyncGroups") and WobbleSyncManager: + print("DEBUG: Loading wobble sync groups. Data: ", data["wobbleSyncGroups"]) + await WobbleSyncManager.loadSaveData(data["wobbleSyncGroups"]) + print("DEBUG: Finished loading wobble sync groups") + # 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() + else: + print("DEBUG: Not loading wobble sync groups - missing data or manager") + + # Final debug check + if WobbleSyncManager: + print("DEBUG: Final check - WobbleSyncManager has ", WobbleSyncManager.getGroupNames().size(), " groups: ", WobbleSyncManager.getGroupNames()) + else: + print("DEBUG: No wobble sync groups to load. has key: ", data.has("wobbleSyncGroups"), ", WobbleSyncManager: ", WobbleSyncManager != null) + + # 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: @@ -546,8 +639,15 @@ func _on_save_dialog_file_selected(path): data[id]["spriteOpacity"] = child.spriteOpacity data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity + # Save wobble sync group + data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup + id += 1 + # Save wobble sync groups + if WobbleSyncManager: + data["wobbleSyncGroups"] = WobbleSyncManager.getSaveData() + Saving.settings["lastAvatar"] = path Saving.data = data.duplicate() @@ -625,6 +725,9 @@ func _on_duplicate_button_pressed(): sprite.spriteOpacity = Global.heldSprite.spriteOpacity sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity + # Copy wobble sync group + sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup + origin.add_child(sprite) sprite.position = Global.heldSprite.position + Vector2(16,16) @@ -676,7 +779,7 @@ func moveSpriteMenu(delta): var size = get_viewport().get_visible_rect().size - var windowLength = 1500 + var windowLength = 1800 $ViewerArrows/Arrows.position.y = size.y - 25 @@ -704,9 +807,9 @@ func moveSpriteMenu(delta): if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area): - Global.spriteEdit.position.y += (delta * 432.0 * physicsTimeMultiplier) + Global.spriteEdit.position.y += (delta * 400.0) elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area): - Global.spriteEdit.position.y -= (delta * 432.0 * physicsTimeMultiplier) + Global.spriteEdit.position.y -= (delta * 400.0) if Global.spriteEdit.position.y > 66: Global.spriteEdit.position.y = 66 diff --git a/main_scenes/main.tscn b/main_scenes/main.tscn index f50bfe5..77a4720 100644 --- a/main_scenes/main.tscn +++ b/main_scenes/main.tscn @@ -14397,6 +14397,20 @@ label_settings = SubResource("LabelSettings_qg0do") z_index = 4090 script = ExtResource("17_bf1ic") +[node name="Container" type="Container" parent="EditControls"] +clip_contents = true +anchors_preset = 9 +anchor_bottom = 1.0 +offset_top = 70.0 +offset_right = 250.0 +offset_bottom = 20070.0 +grow_vertical = 2 +size_flags_vertical = 3 + +[node name="SpriteViewer" parent="EditControls/Container" instance=ExtResource("9_uqv8p")] +z_index = 4094 +position = Vector2(9, 5) + [node name="Add" type="Sprite2D" parent="EditControls"] position = Vector2(104, 32) texture = ExtResource("4_xenui") @@ -14558,11 +14572,6 @@ position = Vector2(2132, 928.5) shape = SubResource("RectangleShape2D_ebmai") disabled = true -[node name="SpriteViewer" parent="EditControls" instance=ExtResource("9_uqv8p")] -visible = false -z_index = 4094 -position = Vector2(9, 66) - [node name="MoveMenuUp" type="Area2D" parent="EditControls"] collision_layer = 0 collision_mask = 2048 diff --git a/project.godot b/project.godot index ccf6086..8c156ed 100644 --- a/project.godot +++ b/project.godot @@ -29,6 +29,7 @@ buses/channel_disable_time=0.0 Saving="*res://autoload/saving.gd" Global="*res://autoload/global.gd" +WobbleSyncManager="*res://autoload/wobble_sync_manager.gd" ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd" DefaultAvatarData="*res://autoload/defaultAvatarData.gd" diff --git a/test_wobble_sync.gd b/test_wobble_sync.gd new file mode 100644 index 0000000..8571f98 --- /dev/null +++ b/test_wobble_sync.gd @@ -0,0 +1,30 @@ +extends Node + +## Test script for wobble sync groups - run this to verify the system is working + +func _ready(): + # Wait a frame for everything to initialize + await get_tree().process_frame + + print("=== Wobble Sync Groups Test ===") + + # Test creating a group + var success = WobbleSyncManager.createGroup("TestGroup") + print("Create group 'TestGroup': ", success) + + # Test getting group names + var groups = WobbleSyncManager.getGroupNames() + print("Available groups: ", groups) + + # Test group existence + print("Group 'TestGroup' exists: ", WobbleSyncManager.hasGroup("TestGroup")) + print("Group 'NonExistent' exists: ", WobbleSyncManager.hasGroup("NonExistent")) + + # Clean up test group + WobbleSyncManager.deleteGroup("TestGroup") + print("Deleted test group") + + groups = WobbleSyncManager.getGroupNames() + print("Groups after deletion: ", groups) + + print("=== Test Complete ===") diff --git a/test_wobble_sync.gd.uid b/test_wobble_sync.gd.uid new file mode 100644 index 0000000..bbd9064 --- /dev/null +++ b/test_wobble_sync.gd.uid @@ -0,0 +1 @@ +uid://ca66ai4v1k0sw diff --git a/ui_scenes/selectedSprite/spriteObject.gd b/ui_scenes/selectedSprite/spriteObject.gd index bc3fcb1..eb47a5f 100644 --- a/ui_scenes/selectedSprite/spriteObject.gd +++ b/ui_scenes/selectedSprite/spriteObject.gd @@ -94,6 +94,9 @@ var affectChildrenOpacity = false #Vis toggle var toggle = "null" +#Wobble Sync Group +var wobbleSyncGroup = "" + func _ready(): Global.main.spriteVisToggles.connect(visToggle) @@ -171,6 +174,8 @@ func _ready(): setClip(clipped) + # Always initialize opacity shader + updateShaderOpacity() if Global.filtering: sprite.texture_filter = 2 @@ -235,6 +240,16 @@ func _process(delta): grabArea.visible = false originSprite.visible = false + # Disable grabArea input and detection when dialogs are open + if Global.main and Global.main.fileSystemOpen: + grabArea.input_pickable = false + grabArea.monitoring = false + grabArea.monitorable = false + else: + grabArea.input_pickable = true + grabArea.monitoring = true + grabArea.monitorable = true + var glob = dragger.global_position if ignoreBounce: glob.y -= Global.main.bounceChange @@ -282,7 +297,12 @@ func talkBlink(): # Only handle talk/blink/edit visibility with modulate # Opacity slider effects are handled by shader sprite.self_modulate.a = 1.0 # Always keep original texture alpha for clipping + var previousAlpha = sprite.modulate.a sprite.modulate.a = baseAlpha # Only apply talk/blink/edit visibility + + # Update shader if modulate alpha changed (for editor dimming) + if previousAlpha != baseAlpha: + updateShaderOpacity() func calculateParentOpacityMultiplier(): var multiplier = 1.0 @@ -296,7 +316,40 @@ func calculateParentOpacityMultiplier(): return multiplier +## Update wobble parameters and sync to group if needed +func updateWobbleParameter(parameter: String, value: float): + print("DEBUG: updateWobbleParameter called - parameter: ", parameter, ", value: ", value, ", sprite ID: ", id) + print("DEBUG: Current wobbleSyncGroup: '", wobbleSyncGroup, "'") + + # Update this sprite's parameter first + match parameter: + "xFrq": xFrq = value + "xAmp": xAmp = value + "yFrq": yFrq = value + "yAmp": yAmp = value + "rFrq": rFrq = value + "rAmp": rAmp = value + + print("DEBUG: Parameter updated on sprite. New values - xFrq:", xFrq, " xAmp:", xAmp, " yFrq:", yFrq, " yAmp:", yAmp, " rFrq:", rFrq, " rAmp:", rAmp) + + # If this sprite is in a sync group, update the entire group + if wobbleSyncGroup != "" and WobbleSyncManager: + print("DEBUG: Sprite is in sync group '", wobbleSyncGroup, "' - updating group wobble parameters") + WobbleSyncManager.updateGroupWobble(wobbleSyncGroup, xFrq, xAmp, yFrq, yAmp, rFrq, rAmp) + + # Don't refresh UI immediately - let the sync process complete first + # The wobble sync control will handle UI updates when needed + else: + print("DEBUG: Sprite not in sync group or WobbleSyncManager not available") + +## Check if this sprite is synced to a group +func isSynced() -> bool: + return wobbleSyncGroup != "" + func delete(): + # Clean up from wobble sync groups + if wobbleSyncGroup != "" and WobbleSyncManager: + WobbleSyncManager.onSpriteDeleted(id) queue_free() func _physics_process(delta): @@ -460,17 +513,17 @@ func updateShaderOpacity(): # Calculate total opacity from user settings and parent hierarchy var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier() - if totalOpacity >= 1.0: - # No shader needed for 100% opacity - sprite.material = null - else: - # Create or update shader material for opacity less than 100% - if opacityMaterial == null: - opacityMaterial = ShaderMaterial.new() - opacityMaterial.shader = opacityShader - - opacityMaterial.set_shader_parameter("opacity", totalOpacity) - sprite.material = opacityMaterial + # Also factor in the modulate alpha for editor dimming/onion skinning + var editorAlpha = sprite.modulate.a + var finalOpacity = totalOpacity * editorAlpha + + # Always apply the opacity shader to ensure it's loaded + if opacityMaterial == null: + opacityMaterial = ShaderMaterial.new() + opacityMaterial.shader = opacityShader + + opacityMaterial.set_shader_parameter("opacity", finalOpacity) + sprite.material = opacityMaterial func updateChildrenOpacityRecursively(): var linkedSprites = getAllLinkedSprites() diff --git a/ui_scenes/settings/settings_menu.gd b/ui_scenes/settings/settings_menu.gd index ade87f1..b8cc18e 100644 --- a/ui_scenes/settings/settings_menu.gd +++ b/ui_scenes/settings/settings_menu.gd @@ -201,8 +201,9 @@ func _on_costume_check_toggled(button_pressed): ## @param button_pressed: bool - Whether StreamDeck should be enabled func _on_stream_deck_check_toggled(button_pressed): Saving.settings["useStreamDeck"] = button_pressed - if ElgatoStreamDeck: - ElgatoStreamDeck.refresh_connection() + var streamdeck_node = get_node_or_null("/root/ElgatoStreamDeck") + if streamdeck_node != null: + streamdeck_node.refresh_connection() Global.pushUpdate("StreamDeck integration " + ("enabled" if button_pressed else "disabled") + ".") ## Handle experimental microphone loudness toggle diff --git a/ui_scenes/spriteEditMenu/sprite_viewer.gd b/ui_scenes/spriteEditMenu/sprite_viewer.gd index 787bd6d..ac94d45 100644 --- a/ui_scenes/spriteEditMenu/sprite_viewer.gd +++ b/ui_scenes/spriteEditMenu/sprite_viewer.gd @@ -9,11 +9,16 @@ extends Node2D @onready var coverCollider = $Area2D/CollisionShape2D - +@onready var wobbleSyncControl = null # Will be assigned in _ready() func _ready(): Global.spriteEdit = self + # Find the wobble sync control node + wobbleSyncControl = find_child("WobbleSyncControl") + if wobbleSyncControl == null: + print("Warning: WobbleSyncControl node not found in sprite viewer") + func setImage(): if Global.heldSprite == null: return @@ -83,6 +88,16 @@ func setImage(): setLayerButtons() + # Update wobble sync control + if wobbleSyncControl: + wobbleSyncControl.setSprite(Global.heldSprite) + # Force a refresh to ensure dropdown is populated + await get_tree().process_frame + wobbleSyncControl.updateUI() + + # Update wobble control states based on sync status + updateWobbleControlStates() + if Global.heldSprite.parentId == null: $Buttons/Unlink.visible = false parentSpin.visible = false @@ -98,6 +113,30 @@ func setImage(): parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y parentSpin.hframes = nodes[0].frames parentSpin.visible = true + +## Update wobble control states based on sync status +func updateWobbleControlStates(): + if Global.heldSprite == null: + return + + var isSynced = Global.heldSprite.isSynced() + + # Keep wobble controls enabled even when synced (they edit group settings) + $WobbleControl/xFrq.editable = true + $WobbleControl/xAmp.editable = true + $WobbleControl/yFrq.editable = true + $WobbleControl/yAmp.editable = true + $WobbleControl/rFrq.editable = true + $WobbleControl/rAmp.editable = true + + # Visual feedback for sync state - slight tint to indicate group editing + var tint = Color(1.0, 1.0, 0.9) if isSynced else Color.WHITE + $WobbleControl/xFrq.modulate = tint + $WobbleControl/xAmp.modulate = tint + $WobbleControl/yFrq.modulate = tint + $WobbleControl/yAmp.modulate = tint + $WobbleControl/rFrq.modulate = tint + $WobbleControl/rAmp.modulate = tint func _process(delta): @@ -131,31 +170,55 @@ func _on_drag_slider_value_changed(value): func _on_x_frq_value_changed(value): + print("DEBUG: _on_x_frq_value_changed called with value: ", value) $WobbleControl/xFrqLabel.text = "x frequency: " + str(value) - Global.heldSprite.xFrq = value + Global.heldSprite.updateWobbleParameter("xFrq", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_x_amp_value_changed(value): + print("DEBUG: _on_x_amp_value_changed called with value: ", value) $WobbleControl/xAmpLabel.text = "x amplitude: " + str(value) - Global.heldSprite.xAmp = value + Global.heldSprite.updateWobbleParameter("xAmp", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_y_frq_value_changed(value): + print("DEBUG: _on_y_frq_value_changed called with value: ", value) $WobbleControl/yFrqLabel.text = "y frequency: " + str(value) - Global.heldSprite.yFrq = value + Global.heldSprite.updateWobbleParameter("yFrq", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_y_amp_value_changed(value): + print("DEBUG: _on_y_amp_value_changed called with value: ", value) $WobbleControl/yAmpLabel.text = "y amplitude: " + str(value) - Global.heldSprite.yAmp = value + Global.heldSprite.updateWobbleParameter("yAmp", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_r_frq_value_changed(value): + print("DEBUG: _on_r_frq_value_changed called with value: ", value) $WobbleControl/rFrqLabel.text = "rotation frequency: " + str(value) - Global.heldSprite.rFrq = value + Global.heldSprite.updateWobbleParameter("rFrq", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_r_amp_value_changed(value): + print("DEBUG: _on_r_amp_value_changed called with value: ", value) $WobbleControl/rAmpLabel.text = "rotation amplitude: " + str(value) + "%" - Global.heldSprite.rAmp = value + Global.heldSprite.updateWobbleParameter("rAmp", value) + # Refresh wobble sync control after parameter change + if wobbleSyncControl: + wobbleSyncControl.updateUI() func _on_r_drag_value_changed(value): @@ -180,6 +243,10 @@ func _on_blinking_pressed(): func _on_trash_pressed(): + # Clean up wobble sync group membership before deleting + if Global.heldSprite.wobbleSyncGroup != "" and WobbleSyncManager: + WobbleSyncManager.onSpriteDeleted(Global.heldSprite.id) + Global.heldSprite.queue_free() Global.heldSprite = null diff --git a/ui_scenes/spriteEditMenu/sprite_viewer.tscn b/ui_scenes/spriteEditMenu/sprite_viewer.tscn index eeb014f..1dcea8e 100644 --- a/ui_scenes/spriteEditMenu/sprite_viewer.tscn +++ b/ui_scenes/spriteEditMenu/sprite_viewer.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=75 format=4 uid="uid://d3anahesvdgfh"] +[gd_scene load_steps=76 format=4 uid="uid://d3anahesvdgfh"] [ext_resource type="Texture2D" uid="uid://4tbpehro4x5p" path="res://ui_scenes/spriteEditMenu/border.png" id="1_b54o8"] [ext_resource type="Script" uid="uid://bgoaqt86bc73p" path="res://ui_scenes/spriteEditMenu/sprite_viewer.gd" id="1_hp0e3"] [ext_resource type="Shader" uid="uid://bfxkb7p0y73yn" path="res://ui_scenes/spriteEditMenu/sprite_viewer.gdshader" id="3_ky0lu"] [ext_resource type="Texture2D" uid="uid://xxhqegm7rq6n" path="res://ui_scenes/button sprites/add.png" id="3_tjnto"] +[ext_resource type="PackedScene" uid="uid://bkwf3j2pdspxw" path="res://ui_scenes/spriteEditMenu/wobble_sync_control.tscn" id="3_wobble_sync"] [ext_resource type="Texture2D" uid="uid://bd68uiemrcy5v" path="res://ui_scenes/spriteEditMenu/speaking.png" id="4_shsbk"] [ext_resource type="Texture2D" uid="uid://doxw3dy86i2e4" path="res://ui_scenes/spriteEditMenu/blink.png" id="5_ed14x"] [ext_resource type="Texture2D" uid="uid://dn5hs0pfutv1l" path="res://ui_scenes/spriteEditMenu/trash.png" id="6_304pj"] @@ -29,7 +30,7 @@ [ext_resource type="Texture2D" uid="uid://gflmeocxfnrq" path="res://ui_scenes/settings/deleteHotkey.png" id="27_fgu6g"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"] -size = Vector2(245, 1455) +size = Vector2(245, 1550) [sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"] shader = ExtResource("3_ky0lu") @@ -14285,7 +14286,7 @@ script = ExtResource("1_hp0e3") z_index = -2 [node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] -position = Vector2(122.5, 727.5) +position = Vector2(122.5, 775) shape = SubResource("RectangleShape2D_21kva") [node name="Border" type="Sprite2D" parent="."] @@ -14469,11 +14470,11 @@ scrollable = false [node name="WobbleControl" type="Node2D" parent="."] z_index = 1 -position = Vector2(-3, 486) +position = Vector2(-3, 491) [node name="animationBox" type="Sprite2D" parent="WobbleControl"] -position = Vector2(125, 384.5) -scale = Vector2(1, 1.25769) +position = Vector2(125, 431) +scale = Vector2(1, 1.65769) texture = ExtResource("15_wqi8b") [node name="xFrqLabel" type="Label" parent="WobbleControl"] @@ -14489,7 +14490,7 @@ offset_top = 263.0 offset_right = 235.0 offset_bottom = 283.0 theme = SubResource("Theme_mrbyn") -max_value = 0.1 +max_value = 1.0 step = 0.001 scrollable = false @@ -14522,7 +14523,7 @@ offset_top = 364.0 offset_right = 235.0 offset_bottom = 384.0 theme = SubResource("Theme_mrbyn") -max_value = 0.1 +max_value = 1.0 step = 0.001 scrollable = false @@ -14555,7 +14556,7 @@ offset_top = 465.0 offset_right = 235.0 offset_bottom = 485.0 theme = SubResource("Theme_mrbyn") -max_value = 0.1 +max_value = 1.0 step = 0.001 scrollable = false @@ -14574,6 +14575,12 @@ offset_bottom = 528.0 theme = SubResource("Theme_mrbyn") scrollable = false +[node name="WobbleSyncControl" parent="WobbleControl" instance=ExtResource("3_wobble_sync")] +offset_left = 12.0 +offset_top = 559.0 +offset_right = 235.0 +offset_bottom = 624.0 + [node name="Rotation" type="Node2D" parent="."] position = Vector2(-1, 224) @@ -14613,7 +14620,7 @@ scrollable = false [node name="RotationalLimits" type="Node2D" parent="."] z_index = -1 -position = Vector2(0, 435) +position = Vector2(0, 532) [node name="RotBack" type="Sprite2D" parent="RotationalLimits"] clip_children = 2 @@ -14762,7 +14769,7 @@ offset_bottom = 1166.0 theme = SubResource("Theme_mrbyn") [node name="Layers" type="Node2D" parent="."] -position = Vector2(0, 443) +position = Vector2(0, 538) [node name="Layer1" type="Sprite2D" parent="Layers"] position = Vector2(23, 878) @@ -14899,7 +14906,7 @@ position = Vector2(23, 878) texture = ExtResource("20_px0dt") [node name="VisToggle" type="Node2D" parent="."] -position = Vector2(11, 1404) +position = Vector2(11, 1496) [node name="setToggle" type="Button" parent="VisToggle"] custom_minimum_size = Vector2(0, 37.125) diff --git a/ui_scenes/spriteEditMenu/wobble_sync_control.gd b/ui_scenes/spriteEditMenu/wobble_sync_control.gd new file mode 100644 index 0000000..0efbfb8 --- /dev/null +++ b/ui_scenes/spriteEditMenu/wobble_sync_control.gd @@ -0,0 +1,207 @@ +extends Control + +## WobbleSyncControl - UI component for managing wobble sync groups +## Designed to integrate into the sprite editor panel + +@onready var groupDropdown = $VBoxContainer/GroupRow/GroupDropdown +@onready var syncIndicator = $VBoxContainer/SyncIndicator +@onready var createGroupDialog = $CreateGroupDialog +@onready var groupNameInput = $CreateGroupDialog/VBoxContainer/LineEdit + +var currentSprite = null +var updating_dropdown = false # Flag to prevent recursive calls + +signal group_changed(sprite_id: int, group_name: String) + +func _ready(): + # Wait for autoload to be ready + await get_tree().process_frame + + # Connect UI signals - no add button to connect + groupDropdown.item_selected.connect(_on_group_dropdown_selected) + createGroupDialog.confirmed.connect(_on_create_group_confirmed) + createGroupDialog.canceled.connect(_on_create_group_cancelled) + + # Connect to wobble sync manager signals + if WobbleSyncManager: + WobbleSyncManager.group_created.connect(_on_group_created) + WobbleSyncManager.group_deleted.connect(_on_group_deleted) + + # Setup create group dialog + createGroupDialog.title = "Create Wobble Sync Group" + createGroupDialog.size = Vector2(300, 150) + createGroupDialog.unresizable = false + createGroupDialog.transient = true # Prevent click-through + createGroupDialog.exclusive = true # Make modal + createGroupDialog.popup_window = false # Keep as embedded dialog + createGroupDialog.always_on_top = true + + updateUI() + +## Update the UI based on current sprite selection +func updateUI(): + updateGroupDropdown() + updateSyncIndicator() + +## Update the group dropdown with available options +func updateGroupDropdown(): + updating_dropdown = true # Prevent recursive calls + + groupDropdown.clear() + + # Add "None" option + groupDropdown.add_item("None") + + # Add existing groups + if WobbleSyncManager: + var groups = WobbleSyncManager.getGroupNames() + print("DEBUG: updateGroupDropdown - available groups: ", groups) + for group_name in groups: + groupDropdown.add_item(group_name) + else: + print("DEBUG: updateGroupDropdown - WobbleSyncManager not available") + + # Add "Create New..." option + groupDropdown.add_separator() + groupDropdown.add_item("Create New...") + + # Select current group if sprite is synced + if currentSprite != null and currentSprite.wobbleSyncGroup != "": + print("DEBUG: updateGroupDropdown - sprite has group '", currentSprite.wobbleSyncGroup, "'") + var group_index = -1 + for i in range(groupDropdown.get_item_count()): + if groupDropdown.get_item_text(i) == currentSprite.wobbleSyncGroup: + group_index = i + break + + if group_index != -1: + print("DEBUG: updateGroupDropdown - selecting group at index ", group_index) + groupDropdown.selected = group_index + else: + print("DEBUG: updateGroupDropdown - group '", currentSprite.wobbleSyncGroup, "' not found in dropdown") + groupDropdown.selected = 0 # Select "None" + else: + print("DEBUG: updateGroupDropdown - sprite has no group, selecting 'None'") + groupDropdown.selected = 0 # Select "None" + + updating_dropdown = false + +## Update the sync indicator +func updateSyncIndicator(): + if currentSprite == null: + syncIndicator.visible = false + return + + if currentSprite.isSynced(): + syncIndicator.visible = true + syncIndicator.text = "⚡ Synced to \"" + currentSprite.wobbleSyncGroup + "\"" + else: + syncIndicator.visible = false + +## Set the current sprite to manage +func setSprite(sprite): + print("DEBUG: WobbleSyncControl.setSprite called with sprite ID: ", sprite.id if sprite else "null") + currentSprite = sprite + if currentSprite: + print("DEBUG: Current sprite wobbleSyncGroup: '", currentSprite.wobbleSyncGroup, "'") + print("DEBUG: Current sprite isSynced(): ", currentSprite.isSynced()) + updateUI() + +## Get available groups for the dropdown (excluding "None" and "Create New...") +func getAvailableGroups() -> Array: + var groups = [] + for i in range(1, groupDropdown.get_item_count() - 2): # Skip "None" and "Create New..." + groups.append(groupDropdown.get_item_text(i)) + return groups + +## Handle group dropdown selection +func _on_group_dropdown_selected(index: int): + if currentSprite == null or updating_dropdown: + return + + var selected_text = groupDropdown.get_item_text(index) + print("DEBUG: Dropdown selected: '", selected_text, "' at index ", index) + + match selected_text: + "None": + # Remove sprite from any group + if currentSprite.wobbleSyncGroup != "" and WobbleSyncManager: + print("DEBUG: Removing sprite ID ", currentSprite.id, " from group '", currentSprite.wobbleSyncGroup, "'") + WobbleSyncManager.removeSpriteFromAllGroups(currentSprite.id) + Global.pushUpdate("Removed sprite from wobble sync group") + + "Create New...": + print("DEBUG: User selected 'Create New...' from dropdown") + # Show create group dialog + groupNameInput.text = "" + groupNameInput.placeholder_text = "Enter group name..." + createGroupDialog.popup_centered_clamped(Vector2i(300, 150)) + groupNameInput.grab_focus() + # Reset dropdown to current selection + updateGroupDropdown() + + _: + # Join selected group + if WobbleSyncManager and WobbleSyncManager.hasGroup(selected_text): + print("DEBUG: Adding sprite ID ", currentSprite.id, " to group '", selected_text, "'") + WobbleSyncManager.addSpriteToGroup(currentSprite.id, selected_text) + # Refresh sprite editor to show updated wobble values + if Global.spriteEdit: + Global.spriteEdit.setImage() + else: + print("DEBUG: Group '", selected_text, "' not found or WobbleSyncManager not available") + + updateSyncIndicator() + +## Handle create group dialog confirmation +func _on_create_group_confirmed(): + var group_name = groupNameInput.text.strip_edges() + print("DEBUG: Create group confirmed with name: '", group_name, "'") + + if group_name == "": + print("DEBUG: Group name is empty") + Global.pushUpdate("Group name cannot be empty") + return + + if WobbleSyncManager and WobbleSyncManager.hasGroup(group_name): + print("DEBUG: Group '", group_name, "' already exists") + Global.pushUpdate("Group \"" + group_name + "\" already exists") + return + + # Create group with current sprite + if currentSprite != null and WobbleSyncManager: + print("DEBUG: Creating group '", group_name, "' with sprite ID ", currentSprite.id) + if WobbleSyncManager.createGroup(group_name, currentSprite.id): + print("DEBUG: Group created successfully") + createGroupDialog.hide() + updateUI() + # Refresh sprite editor to show sync status + if Global.spriteEdit: + Global.spriteEdit.setImage() + else: + print("DEBUG: Failed to create group") + else: + print("DEBUG: No current sprite to add to group") + +## Handle create group dialog cancellation +func _on_create_group_cancelled(): + createGroupDialog.hide() + updateGroupDropdown() # Reset dropdown to current selection + +## Handle group creation signal from manager +func _on_group_created(group_name: String): + updateGroupDropdown() + +## Handle group deletion signal from manager +func _on_group_deleted(group_name: String): + updateGroupDropdown() + updateSyncIndicator() + +## Check if current sprite can have wobble syncing applied +func canSync() -> bool: + return currentSprite != null + +## Force refresh the UI - useful after loading saves +func forceRefresh(): + print("DEBUG: WobbleSyncControl.forceRefresh called") + updateUI() diff --git a/ui_scenes/spriteEditMenu/wobble_sync_control.gd.uid b/ui_scenes/spriteEditMenu/wobble_sync_control.gd.uid new file mode 100644 index 0000000..2823eb2 --- /dev/null +++ b/ui_scenes/spriteEditMenu/wobble_sync_control.gd.uid @@ -0,0 +1 @@ +uid://ykov43vxml0k diff --git a/ui_scenes/spriteEditMenu/wobble_sync_control.tscn b/ui_scenes/spriteEditMenu/wobble_sync_control.tscn new file mode 100644 index 0000000..fe6170d --- /dev/null +++ b/ui_scenes/spriteEditMenu/wobble_sync_control.tscn @@ -0,0 +1,59 @@ +[gd_scene load_steps=2 format=3 uid="uid://bkwf3j2pdspxw"] + +[ext_resource type="Script" uid="uid://ykov43vxml0k" path="res://ui_scenes/spriteEditMenu/wobble_sync_control.gd" id="1_k8h4v"] + +[node name="WobbleSyncControl" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_k8h4v") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="GroupRow" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="GroupLabel" type="Label" parent="VBoxContainer/GroupRow"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Group:" + +[node name="GroupDropdown" type="OptionButton" parent="VBoxContainer/GroupRow"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SyncIndicator" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "⚡ Synced to \"Group Name\"" +autowrap_mode = 2 + +[node name="CreateGroupDialog" type="ConfirmationDialog" parent="."] +process_mode = 3 +title = "Create Wobble Sync Group" +size = Vector2i(300, 150) + +[node name="VBoxContainer" type="VBoxContainer" parent="CreateGroupDialog"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 + +[node name="Label" type="Label" parent="CreateGroupDialog/VBoxContainer"] +layout_mode = 2 +text = "Enter name for new wobble sync group:" + +[node name="LineEdit" type="LineEdit" parent="CreateGroupDialog/VBoxContainer"] +layout_mode = 2 +placeholder_text = "Group name..."