Compare commits

...

11 Commits

21 changed files with 1171 additions and 79 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) ![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 ### ⌨️ Enhanced Modifier Keys
Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fingers. Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fingers.
@@ -29,12 +40,24 @@ Supports combos like Ctrl+Shift+Alt+Cmd. For people with more keybinds than fing
![Modifier Keys](https://github.com/user-attachments/assets/6d492134-f929-4e3e-a6c6-a89cf16cd62f) ![Modifier Keys](https://github.com/user-attachments/assets/6d492134-f929-4e3e-a6c6-a89cf16cd62f)
### ↗️ Middle Mouse Button Panning ### ↗️ Editor Middle Mouse Button Panning
Hold MMB in the editor to pan around the scene, it'll reset when you go back to live. Hold MMB in the editor to pan around the scene, it'll reset when you go back to live.
![Panning](https://github.com/user-attachments/assets/b8687b69-fc84-43c9-950b-70395ae54820) ![Panning](https://github.com/user-attachments/assets/b8687b69-fc84-43c9-950b-70395ae54820)
### ↗️ View Mode MMB Shake
Hold MMB in view mode to shake your character, Show off them physics!
![viewmodeshake](https://github.com/user-attachments/assets/536dc4ae-0562-494f-8cd0-2d2ef115f085)
### 🎞️ Delta Time Physics
Physics are now framerate-independent. Low FPS wont make your character move in slow motion anymore.
Just smooth, consistent jank if thats your thing. Great for stop-motion vibes.
### 🎤 Microphone Fixes ### 🎤 Microphone Fixes
@@ -53,6 +76,7 @@ Updated to **Godot 4.4.1** — no more editor nags or compatibility weirdness.
### 🎚️ StreamDeck Integration ### 🎚️ StreamDeck Integration
StreamDeck plugin was already in, this just wires up the buttons. StreamDeck plugin was already in, this just wires up the buttons.
--- ---
## 🧾 Credits ## 🧾 Credits

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

@@ -4,7 +4,8 @@ extends Node2D
@onready var editButton = $Edit/Button @onready var editButton = $Edit/Button
func _process(delta): func _process(delta):
var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0
if Rect2(editButton.get_parent().position-Vector2(24,24),editButton.size).has_point(get_local_mouse_position()): if Rect2(editButton.get_parent().position-Vector2(24,24),editButton.size).has_point(get_local_mouse_position()):
editSprite.scale = lerp(editSprite.scale,Vector2(1.2,1.2),0.2) editSprite.scale = lerp(editSprite.scale, Vector2(1.2,1.2), delta * uiAnimSpeed)
else: else:
editSprite.scale = lerp(editSprite.scale,Vector2(1.0,1.0),0.2) editSprite.scale = lerp(editSprite.scale, Vector2(1.0,1.0), delta * uiAnimSpeed)

View File

@@ -30,7 +30,8 @@ func _process(delta):
if buttons[b] == null: if buttons[b] == null:
continue continue
if Rect2(buttons[b].get_parent().position-Vector2(24,24),buttons[b].size).has_point(get_local_mouse_position()): if Rect2(buttons[b].get_parent().position-Vector2(24,24),buttons[b].size).has_point(get_local_mouse_position()):
sprites[s].scale = lerp(sprites[s].scale,Vector2(1.2,1.2),0.2) var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0
sprites[s].scale = lerp(sprites[s].scale, Vector2(1.2,1.2), delta * uiAnimSpeed)
if Input.is_action_pressed("mouse_left"): if Input.is_action_pressed("mouse_left"):
sprites[s].scale = Vector2(0.6,0.6) sprites[s].scale = Vector2(0.6,0.6)
match b: match b:
@@ -50,7 +51,8 @@ func _process(delta):
Global.mouse.text = "Duplicate sprite" Global.mouse.text = "Duplicate sprite"
else: else:
sprites[s].scale = lerp(sprites[s].scale,Vector2(1.0,1.0),0.2) var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0
sprites[s].scale = lerp(sprites[s].scale, Vector2(1.0,1.0), delta * uiAnimSpeed)
s += 1 s += 1
var newColor = Color.DARK_SLATE_GRAY if Global.heldSprite == null else Color.WHITE var newColor = Color.DARK_SLATE_GRAY if Global.heldSprite == null else Color.WHITE

View File

@@ -36,6 +36,10 @@ var yVel = 0
var bounceSlider = 250 var bounceSlider = 250
var bounceGravity = 1000 var bounceGravity = 1000
# Physics timing multiplier - adjust this to fine-tune physics speed
# 1.0 = very slow, 10.0 = reasonable, 60.0 = old frame-based speed
var physicsTimeMultiplier = 6.0
#Costumes #Costumes
var costume = 1 var costume = 1
var bounceOnCostumeChange = false var bounceOnCostumeChange = false
@@ -49,6 +53,12 @@ var initialPanPosition = Vector2.ZERO
var initialCameraOffset = Vector2.ZERO var initialCameraOffset = Vector2.ZERO
var cameraOffset = Vector2.ZERO var cameraOffset = Vector2.ZERO
#View Mode Panning
var viewModePanning = false
var initialViewPanPosition = Vector2.ZERO
var viewPanOffset = Vector2.ZERO
var viewPanLerpSpeed = 3.0
var bounceChange = 0.0 var bounceChange = 0.0
#IMPORTANT #IMPORTANT
@@ -68,14 +78,27 @@ func _ready():
Global.connect("startSpeaking",onSpeak) 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"]: if Saving.settings["newUser"]:
print("DEBUG: MAIN._ready() - New user detected, loading default avatar")
_on_load_dialog_file_selected("default") _on_load_dialog_file_selected("default")
Saving.settings["newUser"] = false Saving.settings["newUser"] = false
saveLoaded = true saveLoaded = true
else: 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/volumeSlider.value = Saving.settings["volume"]
$ControlPanel/sensitiveSlider.value = Saving.settings["sense"] $ControlPanel/sensitiveSlider.value = Saving.settings["sense"]
@@ -141,18 +164,28 @@ func _ready():
func _process(delta): func _process(delta):
var hold = origin.get_parent().position.y var hold = origin.get_parent().position.y
origin.get_parent().position.y += yVel * 0.0166 origin.get_parent().position.y += yVel * delta * physicsTimeMultiplier
if origin.get_parent().position.y > 0: if origin.get_parent().position.y > 0:
origin.get_parent().position.y = 0 origin.get_parent().position.y = 0
bounceChange = hold - origin.get_parent().position.y bounceChange = hold - origin.get_parent().position.y
yVel += bounceGravity*0.0166 yVel += bounceGravity * delta * physicsTimeMultiplier
# Handle view mode panning lerp back to center
if !editMode:
if !viewModePanning:
# Lerp back to center when not actively panning
viewPanOffset = viewPanOffset.lerp(Vector2.ZERO, viewPanLerpSpeed * delta)
# Apply view pan offset to the root character (affects physics)
origin.position = (get_viewport().get_visible_rect().size * 0.5) + viewPanOffset
updateCameraPosition()
if Input.is_action_just_pressed("openFolder"): if Input.is_action_just_pressed("openFolder"):
OS.shell_open(ProjectSettings.globalize_path("user://")) OS.shell_open(ProjectSettings.globalize_path("user://"))
moveSpriteMenu(delta) moveSpriteMenu(delta)
zoomScene() zoomScene(delta)
fileSystemOpen = isFileSystemOpen() fileSystemOpen = isFileSystemOpen()
@@ -173,15 +206,24 @@ func _input(event):
isPanning = false isPanning = false
else: else:
if event.pressed: if event.pressed:
# Reset camera in view mode # Start view mode panning
resetCameraPosition() viewModePanning = true
initialViewPanPosition = event.global_position
else:
# Stop view mode panning
viewModePanning = false
# Handle mouse movement during panning # Handle mouse movement during edit mode panning
elif editMode and event is InputEventMouseMotion and isPanning: elif editMode and event is InputEventMouseMotion and isPanning:
var totalDelta = event.global_position - initialPanPosition var totalDelta = event.global_position - initialPanPosition
# Calculate new camera offset from initial position plus total movement # Scale pan distance based on zoom level for more intuitive panning
cameraOffset = initialCameraOffset - totalDelta # Higher zoom (closer) = less movement, lower zoom (farther) = more movement
var zoomScale = 1.0 / camera.zoom.x
var scaledDelta = totalDelta * zoomScale
# Calculate new camera offset from initial position plus scaled movement
cameraOffset = initialCameraOffset - scaledDelta
# Update camera position immediately # Update camera position immediately
var s = get_viewport().get_visible_rect().size var s = get_viewport().get_visible_rect().size
@@ -194,6 +236,15 @@ func _input(event):
viewerArrows.position = editControls.position viewerArrows.position = editControls.position
pushUpdates.position.y = controlPanel.position.y pushUpdates.position.y = controlPanel.position.y
pushUpdates.position.x = editControls.position.x pushUpdates.position.x = editControls.position.x
# Handle mouse movement during view mode panning
elif !editMode and event is InputEventMouseMotion and viewModePanning:
var totalDelta = event.global_position - initialViewPanPosition
# Scale pan distance for view mode (less sensitive than edit mode)
# Invert the movement so dragging right moves character right (not left)
var panSensitivity = 0.5
viewPanOffset = -totalDelta * panSensitivity
func followShadow(): func followShadow():
# Shadow disabled for selected sprites # Shadow disabled for selected sprites
@@ -207,6 +258,11 @@ func isFileSystemOpen():
return true return true
Global.heldSprite = null Global.heldSprite = null
return true 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 return false
#Displays control panel whether or not application is focused #Displays control panel whether or not application is focused
@@ -235,7 +291,7 @@ func onWindowSizeChange():
updateCameraPosition() updateCameraPosition()
spriteList.position.x = s.x - 233 spriteList.position.x = s.x - 233
func zoomScene(): func zoomScene(delta):
#Handles Zooming #Handles Zooming
if Input.is_action_pressed("control"): if Input.is_action_pressed("control"):
if Input.is_action_just_pressed("scrollUp"): if Input.is_action_just_pressed("scrollUp"):
@@ -249,7 +305,7 @@ func zoomScene():
scaleOverall -= 10 scaleOverall -= 10
changeZoom() changeZoom()
$ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a,0.0,0.02) $ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a, 0.0, delta * (physicsTimeMultiplier * 1.2))
func changeZoom(): func changeZoom():
var newZoom = Vector2(1.0,1.0) / camera.zoom var newZoom = Vector2(1.0,1.0) / camera.zoom
@@ -296,6 +352,8 @@ func updateCameraPosition():
func resetCameraPosition(): func resetCameraPosition():
cameraOffset = Vector2.ZERO cameraOffset = Vector2.ZERO
isPanning = false isPanning = false
viewPanOffset = Vector2.ZERO
viewModePanning = false
updateCameraPosition() updateCameraPosition()
Global.pushUpdate("Camera reset to center.") Global.pushUpdate("Camera reset to center.")
@@ -315,6 +373,10 @@ func swapMode():
# Reset camera when entering view mode # Reset camera when entering view mode
if !editMode: if !editMode:
resetCameraPosition() resetCameraPosition()
else:
# Reset view mode panning when entering edit mode
viewPanOffset = Vector2.ZERO
viewModePanning = false
get_viewport().transparent_bg = !editMode get_viewport().transparent_bg = !editMode
if Global.backgroundColor.a != 0.0: if Global.backgroundColor.a != 0.0:
@@ -363,17 +425,52 @@ func _on_load_button_pressed():
#LOAD AVATAR #LOAD AVATAR
func _on_load_dialog_file_selected(path): 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) var data = Saving.read_save(path)
print("DEBUG: Save data read. Data type: ", typeof(data), ", null check: ", data == null)
if data == null: if data == null:
print("DEBUG: Save data is null, returning")
return 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() origin.queue_free()
var new = Node2D.new() var new = Node2D.new()
$OriginMotion.add_child(new) $OriginMotion.add_child(new)
origin = new origin = new
for item in data: 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() var sprite = spriteObject.instantiate()
sprite.path = data[item]["path"] sprite.path = data[item]["path"]
sprite.id = data[item]["identification"] sprite.id = data[item]["identification"]
@@ -431,6 +528,10 @@ func _on_load_dialog_file_selected(path):
if data[item].has("affectChildrenOpacity"): if data[item].has("affectChildrenOpacity"):
sprite.affectChildrenOpacity = data[item]["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) origin.add_child(sprite)
sprite.position = str_to_var(data[item]["pos"]) sprite.position = str_to_var(data[item]["pos"])
@@ -438,7 +539,43 @@ func _on_load_dialog_file_selected(path):
Saving.settings["lastAvatar"] = path Saving.settings["lastAvatar"] = path
Global.spriteList.updateData() 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 await get_tree().process_frame # Wait for all sprites to be fully initialized
var allSprites = get_tree().get_nodes_in_group("saved") var allSprites = get_tree().get_nodes_in_group("saved")
for sprite in allSprites: for sprite in allSprites:
@@ -502,8 +639,15 @@ func _on_save_dialog_file_selected(path):
data[id]["spriteOpacity"] = child.spriteOpacity data[id]["spriteOpacity"] = child.spriteOpacity
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
# Save wobble sync group
data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup
id += 1 id += 1
# Save wobble sync groups
if WobbleSyncManager:
data["wobbleSyncGroups"] = WobbleSyncManager.getSaveData()
Saving.settings["lastAvatar"] = path Saving.settings["lastAvatar"] = path
Saving.data = data.duplicate() Saving.data = data.duplicate()
@@ -581,6 +725,9 @@ func _on_duplicate_button_pressed():
sprite.spriteOpacity = Global.heldSprite.spriteOpacity sprite.spriteOpacity = Global.heldSprite.spriteOpacity
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
# Copy wobble sync group
sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup
origin.add_child(sprite) origin.add_child(sprite)
sprite.position = Global.heldSprite.position + Vector2(16,16) sprite.position = Global.heldSprite.position + Vector2(16,16)
@@ -632,7 +779,7 @@ func moveSpriteMenu(delta):
var size = get_viewport().get_visible_rect().size var size = get_viewport().get_visible_rect().size
var windowLength = 1500 var windowLength = 1800
$ViewerArrows/Arrows.position.y = size.y - 25 $ViewerArrows/Arrows.position.y = size.y - 25
@@ -660,9 +807,9 @@ func moveSpriteMenu(delta):
if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area): if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area):
Global.spriteEdit.position.y += (delta*432.0) Global.spriteEdit.position.y += (delta * 400.0)
elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area): elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area):
Global.spriteEdit.position.y -= (delta*432.0) Global.spriteEdit.position.y -= (delta * 400.0)
if Global.spriteEdit.position.y > 66: if Global.spriteEdit.position.y > 66:
Global.spriteEdit.position.y = 66 Global.spriteEdit.position.y = 66

View File

@@ -14358,6 +14358,7 @@ text = "Zoom : 100%"
horizontal_alignment = 2 horizontal_alignment = 2
[node name="VersionLabels" type="Node2D" parent="ControlPanel"] [node name="VersionLabels" type="Node2D" parent="ControlPanel"]
position = Vector2(-284, 59)
[node name="Label" type="Label" parent="ControlPanel/VersionLabels"] [node name="Label" type="Label" parent="ControlPanel/VersionLabels"]
offset_left = -713.0 offset_left = -713.0
@@ -14377,17 +14378,39 @@ label_settings = SubResource("LabelSettings_qg0do")
[node name="versionNo" type="Label" parent="ControlPanel/VersionLabels/Label2"] [node name="versionNo" type="Label" parent="ControlPanel/VersionLabels/Label2"]
layout_mode = 0 layout_mode = 0
offset_left = 144.0 offset_left = 104.0
offset_top = 40.0 offset_top = 11.0
offset_right = 282.0 offset_right = 242.0
offset_bottom = 74.0 offset_bottom = 54.0
text = "v1.4.5" text = "v1.4.5"
label_settings = SubResource("LabelSettings_xvf50") label_settings = SubResource("LabelSettings_xvf50")
[node name="Label3" type="Label" parent="ControlPanel/VersionLabels"]
offset_left = -566.0
offset_top = -93.0
offset_right = -428.0
offset_bottom = -59.0
text = "Litruv Edition"
label_settings = SubResource("LabelSettings_qg0do")
[node name="EditControls" type="Node2D" parent="."] [node name="EditControls" type="Node2D" parent="."]
z_index = 4090 z_index = 4090
script = ExtResource("17_bf1ic") 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"] [node name="Add" type="Sprite2D" parent="EditControls"]
position = Vector2(104, 32) position = Vector2(104, 32)
texture = ExtResource("4_xenui") texture = ExtResource("4_xenui")
@@ -14549,11 +14572,6 @@ position = Vector2(2132, 928.5)
shape = SubResource("RectangleShape2D_ebmai") shape = SubResource("RectangleShape2D_ebmai")
disabled = true 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"] [node name="MoveMenuUp" type="Area2D" parent="EditControls"]
collision_layer = 0 collision_layer = 0
collision_mask = 2048 collision_mask = 2048

View File

@@ -29,6 +29,7 @@ buses/channel_disable_time=0.0
Saving="*res://autoload/saving.gd" Saving="*res://autoload/saving.gd"
Global="*res://autoload/global.gd" Global="*res://autoload/global.gd"
WobbleSyncManager="*res://autoload/wobble_sync_manager.gd"
ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd" ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd"
DefaultAvatarData="*res://autoload/defaultAvatarData.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

@@ -79,12 +79,13 @@ var ignoreBounce = false
#Animation #Animation
var frames = 1 var frames = 1
var animSpeed = 0 var animSpeed = 0
var animTime = 0.0 # Time accumulator for animations
var remadePolygon = false var remadePolygon = false
var clipped = false var clipped = false
var tick = 0 var time = 0.0 # Time accumulator for physics calculations
#Opacity #Opacity
var spriteOpacity = 1.0 var spriteOpacity = 1.0
@@ -93,6 +94,9 @@ var affectChildrenOpacity = false
#Vis toggle #Vis toggle
var toggle = "null" var toggle = "null"
#Wobble Sync Group
var wobbleSyncGroup = ""
func _ready(): func _ready():
Global.main.spriteVisToggles.connect(visToggle) Global.main.spriteVisToggles.connect(visToggle)
@@ -170,6 +174,8 @@ func _ready():
setClip(clipped) setClip(clipped)
# Always initialize opacity shader
updateShaderOpacity()
if Global.filtering: if Global.filtering:
sprite.texture_filter = 2 sprite.texture_filter = 2
@@ -224,7 +230,7 @@ func replaceSprite(pathNew):
remakePolygon() remakePolygon()
func _process(delta): func _process(delta):
tick += 1 time += delta
if Global.heldSprite == self: if Global.heldSprite == self:
grabArea.visible = true grabArea.visible = true
@@ -234,6 +240,16 @@ func _process(delta):
grabArea.visible = false grabArea.visible = false
originSprite.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 var glob = dragger.global_position
if ignoreBounce: if ignoreBounce:
glob.y -= Global.main.bounceChange glob.y -= Global.main.bounceChange
@@ -251,13 +267,17 @@ func _process(delta):
talkBlink() talkBlink()
animation() animation(delta)
func animation(): func animation(delta):
var speed = max(float(animSpeed),Engine.max_fps*6.0)
if animSpeed > 0 and frames > 1: if animSpeed > 0 and frames > 1:
if Global.animationTick % int((speed)/float(animSpeed)) == 0: animTime += delta
# Convert animSpeed to frames per second - no additional scaling needed since this controls frame rate
var frameRate = animSpeed / 60.0 # Normalize from old frame-based system
var frameInterval = 1.0 / frameRate
if animTime >= frameInterval:
animTime -= frameInterval
if sprite.frame == frames - 1: if sprite.frame == frames - 1:
sprite.frame = 0 sprite.frame = 0
else: else:
@@ -277,7 +297,12 @@ func talkBlink():
# Only handle talk/blink/edit visibility with modulate # Only handle talk/blink/edit visibility with modulate
# Opacity slider effects are handled by shader # Opacity slider effects are handled by shader
sprite.self_modulate.a = 1.0 # Always keep original texture alpha for clipping 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 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(): func calculateParentOpacityMultiplier():
var multiplier = 1.0 var multiplier = 1.0
@@ -291,7 +316,40 @@ func calculateParentOpacityMultiplier():
return multiplier 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(): func delete():
# Clean up from wobble sync groups
if wobbleSyncGroup != "" and WobbleSyncManager:
WobbleSyncManager.onSpriteDeleted(id)
queue_free() queue_free()
func _physics_process(delta): func _physics_process(delta):
@@ -348,31 +406,46 @@ func drag(delta):
if dragSpeed == 0: if dragSpeed == 0:
dragger.global_position = wob.global_position dragger.global_position = wob.global_position
else: else:
dragger.global_position = lerp(dragger.global_position,wob.global_position,1/dragSpeed) # Convert dragSpeed to a proper delta-time lerp factor
# Higher dragSpeed = slower movement, so we invert it
# Use Global.main.physicsTimeMultiplier for consistent physics timing
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
var lerpFactor = clamp(delta * (physicsMultiplier * 10.0 / max(dragSpeed, 0.1)), 0.0, 1.0)
dragger.global_position = lerp(dragger.global_position, wob.global_position, lerpFactor)
dragOrigin.global_position = dragger.global_position dragOrigin.global_position = dragger.global_position
func wobble(): func wobble():
wob.position.x = sin(tick*xFrq)*xAmp # Use Global.main.physicsTimeMultiplier for consistent physics timing
wob.position.y = sin(tick*yFrq)*yAmp var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
var scaledTime = time * physicsMultiplier
wob.position.x = sin(scaledTime * xFrq) * xAmp
wob.position.y = sin(scaledTime * yFrq) * yAmp
func rotationalDrag(length,delta): func rotationalDrag(length, delta):
var yvel = (length * rdragStr) var yvel = (length * rdragStr)
#Calculate Max angle #Calculate Max angle
yvel = clamp(yvel,rLimitMin,rLimitMax) yvel = clamp(yvel, rLimitMin, rLimitMax)
# Add rotation wobble as percentage of rotational limits range # Add rotation wobble as percentage of rotational limits range
var rotationRange = rLimitMax - rLimitMin var rotationRange = rLimitMax - rLimitMin
var rotationWobble = sin(tick*rFrq) * (rotationRange * rAmp * 0.01) var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
var scaledTime = time * physicsMultiplier
var rotationWobble = sin(scaledTime * rFrq) * (rotationRange * rAmp * 0.01)
var totalRotation = yvel + rotationWobble var totalRotation = yvel + rotationWobble
sprite.rotation = lerp_angle(sprite.rotation,deg_to_rad(totalRotation),0.25) # Use delta-based lerp for smooth rotation with slower speed
var lerpFactor = clamp(delta * (physicsMultiplier * 1.0), 0.0, 1.0) # Reduced from 15.0 to 4.0
sprite.rotation = lerp_angle(sprite.rotation, deg_to_rad(totalRotation), lerpFactor)
func stretch(length,delta): func stretch(length, delta):
var yvel = (length * stretchAmount * 0.01) var yvel = (length * stretchAmount * 0.01)
var target = Vector2(1.0-yvel,1.0+yvel) var target = Vector2(1.0 - yvel, 1.0 + yvel)
sprite.scale = lerp(sprite.scale,target,0.5) # Use delta-based lerp for smooth stretching
var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
var lerpFactor = clamp(delta * (physicsMultiplier * 30.0), 0.0, 1.0)
sprite.scale = lerp(sprite.scale, target, lerpFactor)
func changeCollision(enable): func changeCollision(enable):
grabArea.monitorable = enable grabArea.monitorable = enable
@@ -440,17 +513,17 @@ func updateShaderOpacity():
# Calculate total opacity from user settings and parent hierarchy # Calculate total opacity from user settings and parent hierarchy
var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier() var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier()
if totalOpacity >= 1.0: # Also factor in the modulate alpha for editor dimming/onion skinning
# No shader needed for 100% opacity var editorAlpha = sprite.modulate.a
sprite.material = null var finalOpacity = totalOpacity * editorAlpha
else:
# Create or update shader material for opacity less than 100% # Always apply the opacity shader to ensure it's loaded
if opacityMaterial == null: if opacityMaterial == null:
opacityMaterial = ShaderMaterial.new() opacityMaterial = ShaderMaterial.new()
opacityMaterial.shader = opacityShader opacityMaterial.shader = opacityShader
opacityMaterial.set_shader_parameter("opacity", totalOpacity) opacityMaterial.set_shader_parameter("opacity", finalOpacity)
sprite.material = opacityMaterial sprite.material = opacityMaterial
func updateChildrenOpacityRecursively(): func updateChildrenOpacityRecursively():
var linkedSprites = getAllLinkedSprites() 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 ## @param button_pressed: bool - Whether StreamDeck should be enabled
func _on_stream_deck_check_toggled(button_pressed): func _on_stream_deck_check_toggled(button_pressed):
Saving.settings["useStreamDeck"] = button_pressed Saving.settings["useStreamDeck"] = button_pressed
if ElgatoStreamDeck: var streamdeck_node = get_node_or_null("/root/ElgatoStreamDeck")
ElgatoStreamDeck.refresh_connection() if streamdeck_node != null:
streamdeck_node.refresh_connection()
Global.pushUpdate("StreamDeck integration " + ("enabled" if button_pressed else "disabled") + ".") Global.pushUpdate("StreamDeck integration " + ("enabled" if button_pressed else "disabled") + ".")
## Handle experimental microphone loudness toggle ## Handle experimental microphone loudness toggle

View File

@@ -9,11 +9,16 @@ extends Node2D
@onready var coverCollider = $Area2D/CollisionShape2D @onready var coverCollider = $Area2D/CollisionShape2D
@onready var wobbleSyncControl = null # Will be assigned in _ready()
func _ready(): func _ready():
Global.spriteEdit = self 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(): func setImage():
if Global.heldSprite == null: if Global.heldSprite == null:
return return
@@ -83,6 +88,16 @@ func setImage():
setLayerButtons() 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: if Global.heldSprite.parentId == null:
$Buttons/Unlink.visible = false $Buttons/Unlink.visible = false
parentSpin.visible = false parentSpin.visible = false
@@ -98,6 +113,30 @@ func setImage():
parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y
parentSpin.hframes = nodes[0].frames parentSpin.hframes = nodes[0].frames
parentSpin.visible = true 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): func _process(delta):
@@ -131,31 +170,55 @@ func _on_drag_slider_value_changed(value):
func _on_x_frq_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) $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): 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) $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): 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) $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): 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) $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): 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) $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): 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) + "%" $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): func _on_r_drag_value_changed(value):
@@ -180,6 +243,10 @@ func _on_blinking_pressed():
func _on_trash_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.queue_free()
Global.heldSprite = null 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="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="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="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="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://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://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"] [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"] [ext_resource type="Texture2D" uid="uid://gflmeocxfnrq" path="res://ui_scenes/settings/deleteHotkey.png" id="27_fgu6g"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"]
size = Vector2(245, 1455) size = Vector2(245, 1550)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"]
shader = ExtResource("3_ky0lu") shader = ExtResource("3_ky0lu")
@@ -14285,7 +14286,7 @@ script = ExtResource("1_hp0e3")
z_index = -2 z_index = -2
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] [node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2(122.5, 727.5) position = Vector2(122.5, 775)
shape = SubResource("RectangleShape2D_21kva") shape = SubResource("RectangleShape2D_21kva")
[node name="Border" type="Sprite2D" parent="."] [node name="Border" type="Sprite2D" parent="."]
@@ -14469,11 +14470,11 @@ scrollable = false
[node name="WobbleControl" type="Node2D" parent="."] [node name="WobbleControl" type="Node2D" parent="."]
z_index = 1 z_index = 1
position = Vector2(-3, 486) position = Vector2(-3, 491)
[node name="animationBox" type="Sprite2D" parent="WobbleControl"] [node name="animationBox" type="Sprite2D" parent="WobbleControl"]
position = Vector2(125, 384.5) position = Vector2(125, 431)
scale = Vector2(1, 1.25769) scale = Vector2(1, 1.65769)
texture = ExtResource("15_wqi8b") texture = ExtResource("15_wqi8b")
[node name="xFrqLabel" type="Label" parent="WobbleControl"] [node name="xFrqLabel" type="Label" parent="WobbleControl"]
@@ -14489,7 +14490,7 @@ offset_top = 263.0
offset_right = 235.0 offset_right = 235.0
offset_bottom = 283.0 offset_bottom = 283.0
theme = SubResource("Theme_mrbyn") theme = SubResource("Theme_mrbyn")
max_value = 0.1 max_value = 1.0
step = 0.001 step = 0.001
scrollable = false scrollable = false
@@ -14522,7 +14523,7 @@ offset_top = 364.0
offset_right = 235.0 offset_right = 235.0
offset_bottom = 384.0 offset_bottom = 384.0
theme = SubResource("Theme_mrbyn") theme = SubResource("Theme_mrbyn")
max_value = 0.1 max_value = 1.0
step = 0.001 step = 0.001
scrollable = false scrollable = false
@@ -14555,7 +14556,7 @@ offset_top = 465.0
offset_right = 235.0 offset_right = 235.0
offset_bottom = 485.0 offset_bottom = 485.0
theme = SubResource("Theme_mrbyn") theme = SubResource("Theme_mrbyn")
max_value = 0.1 max_value = 1.0
step = 0.001 step = 0.001
scrollable = false scrollable = false
@@ -14574,6 +14575,12 @@ offset_bottom = 528.0
theme = SubResource("Theme_mrbyn") theme = SubResource("Theme_mrbyn")
scrollable = false 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="."] [node name="Rotation" type="Node2D" parent="."]
position = Vector2(-1, 224) position = Vector2(-1, 224)
@@ -14613,7 +14620,7 @@ scrollable = false
[node name="RotationalLimits" type="Node2D" parent="."] [node name="RotationalLimits" type="Node2D" parent="."]
z_index = -1 z_index = -1
position = Vector2(0, 435) position = Vector2(0, 532)
[node name="RotBack" type="Sprite2D" parent="RotationalLimits"] [node name="RotBack" type="Sprite2D" parent="RotationalLimits"]
clip_children = 2 clip_children = 2
@@ -14762,7 +14769,7 @@ offset_bottom = 1166.0
theme = SubResource("Theme_mrbyn") theme = SubResource("Theme_mrbyn")
[node name="Layers" type="Node2D" parent="."] [node name="Layers" type="Node2D" parent="."]
position = Vector2(0, 443) position = Vector2(0, 538)
[node name="Layer1" type="Sprite2D" parent="Layers"] [node name="Layer1" type="Sprite2D" parent="Layers"]
position = Vector2(23, 878) position = Vector2(23, 878)
@@ -14899,7 +14906,7 @@ position = Vector2(23, 878)
texture = ExtResource("20_px0dt") texture = ExtResource("20_px0dt")
[node name="VisToggle" type="Node2D" parent="."] [node name="VisToggle" type="Node2D" parent="."]
position = Vector2(11, 1404) position = Vector2(11, 1496)
[node name="setToggle" type="Button" parent="VisToggle"] [node name="setToggle" type="Button" parent="VisToggle"]
custom_minimum_size = Vector2(0, 37.125) 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..."