wobble sync groups !build

This commit is contained in:
2025-07-20 10:24:53 +10:00
parent 5dfe68cb81
commit cebef2f06b
19 changed files with 1046 additions and 43 deletions

View File

@@ -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.

View File

@@ -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 ===")

View File

@@ -0,0 +1 @@
uid://ccl0r2eov68sy

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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"

30
test_wobble_sync.gd Normal file
View File

@@ -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 ===")

1
test_wobble_sync.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://ca66ai4v1k0sw

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -0,0 +1 @@
uid://ykov43vxml0k

View File

@@ -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..."