Compare commits

...

19 Commits

Author SHA1 Message Date
92fc5dda89 removed temp files and removed git plugin !build 2025-07-21 06:35:50 +10:00
af17a1dee0 Fixed up edit menu. now is more modular + uses extensive use of v+hbox's !build 2025-07-21 06:21:42 +10:00
43dfc410dc added blend modes 2025-07-20 22:14:42 +10:00
ef8ecb3732 !build 2025-07-20 10:26:51 +10:00
803e867e20 Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 10:25:09 +10:00
cebef2f06b wobble sync groups !build 2025-07-20 10:24:53 +10:00
e0420a6583 Update README.md 2025-07-20 07:35:21 +10:00
56e76c61e6 Update README.md !build 2025-07-20 07:34:46 +10:00
5dfe68cb81 Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 07:32:01 +10:00
6e02a67114 replaced framerate dependant physics with delta time 2025-07-20 07:31:59 +10:00
32a67a6617 Update README.md !build 2025-07-20 06:57:22 +10:00
fb0c7a9e3b Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 06:53:37 +10:00
f0ce00b23b Viewmode Panning 2025-07-20 06:53:34 +10:00
d4a9f5a0b0 scaled zooming 2025-07-20 06:36:07 +10:00
085d6a21a0 Update README.md !build 2025-07-20 06:33:37 +10:00
99d2d3f508 Update UI positions during zoom changes 2025-07-20 06:26:23 +10:00
dad22ece22 !build fixed opacity shader on open 2025-07-20 06:19:21 +10:00
52496df10e added panning to edit mode 2025-07-20 06:13:09 +10:00
29b903fadd Improve GitHub Actions build automation workflow
- Update build automation wording and descriptions
- Refine workflow trigger conditions
- Enhance build process testing and validation
- Consolidate multiple build automation improvements
2025-07-20 05:48:34 +10:00
58 changed files with 18286 additions and 672 deletions

View File

@@ -136,34 +136,38 @@ jobs:
# Generate commit history since last tag (or all commits if no tag)
$commitRange = if ($latestTag) { "$latestTag..HEAD" } else { "HEAD" }
$commitHistory = ""
# Get commits with format: short hash, subject, author
$commits = & git log $commitRange --pretty=format:"%h|%s|%an" --no-merges
# Build commit history as a single string
$commitHistory = ""
if ($commits) {
$commitHistory = "`n`n## Changes Since Last Release`n"
$commitHistory += "`n`n## Changes Since Last Release`n`n"
$commits | ForEach-Object {
$parts = $_ -split '\|', 3
$hash = $parts[0]
$subject = $parts[1]
$subject = $parts[1] -replace '"', '""'
$author = $parts[2]
$commitUrl = "https://github.com/${{ github.repository }}/commit/$hash"
$commitHistory += "- [$hash]($commitUrl) $subject (by $author)`n"
$commitHistory += "- [$hash]($commitUrl) $subject`n"
}
} else {
$commitHistory = "`n`n## Changes Since Last Release`nNo new commits since last release.`n"
$commitHistory += "`n`n## Changes Since Last Release`n`nNo new commits since last release.`n"
}
# Escape special characters for GitHub output
$commitHistoryEscaped = $commitHistory -replace '"', '\"' -replace '`', '\`'
# Set outputs for use in next steps
echo "tag=$tag" >> $env:GITHUB_OUTPUT
echo "release_name=$releaseName" >> $env:GITHUB_OUTPUT
echo "short_sha=$shortSha" >> $env:GITHUB_OUTPUT
echo "build_number=$buildNumber" >> $env:GITHUB_OUTPUT
echo "commit_history=$commitHistoryEscaped" >> $env:GITHUB_OUTPUT
# Use heredoc syntax for multiline commit history
Add-Content -Path $env:GITHUB_OUTPUT -Value "commit_history<<EOF"
Add-Content -Path $env:GITHUB_OUTPUT -Value $commitHistory
Add-Content -Path $env:GITHUB_OUTPUT -Value "EOF"
- name: Download Windows Build
uses: actions/download-artifact@v4
with:
@@ -181,10 +185,6 @@ jobs:
body: |
Automated build of PNGTuber Plus
**Build Information:**
- Build Number: ${{ steps.release_info.outputs.build_number }}
- Commit: [${{ steps.release_info.outputs.short_sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
- Branch: ${{ github.ref_name }}
${{ steps.release_info.outputs.commit_history }}
## Installation
@@ -192,9 +192,6 @@ jobs:
2. Run the executable - no installation required!
3. For StreamDeck integration, download the plugin from [here](https://github.com/BoyneGames/streamdeck-godot-plugin/releases/tag/1.0.0)
## Credits
- Original project by [kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
- Microphone improvements by [k0ffinz](https://github.com/k0ffinz/PNGTuber-Plus)
draft: false
prerelease: false

2
.vscode/tasks.json vendored
View File

@@ -4,7 +4,7 @@
{
"label": "Build PNGTuber-Plus",
"type": "shell",
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus && .\\build-windows.bat",
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus; .\\build-windows.bat",
"group": "build"
}
]

View File

@@ -15,13 +15,32 @@ Added a proper opacity slider for sprites, with an "affect children" toggle. Cli
![Opacity Demo](https://github.com/user-attachments/assets/d5a05508-1cb1-46e0-8c90-d0aac73398db)
### 🌀 Rotation Wobble
### <EFBFBD> Blend Modes
Photoshop-style blend modes for sprites! Choose from Normal, Multiply, Screen, Overlay, Soft Light, Hard Light, Color Dodge, Color Burn, Darken, Lighten, Add, and Subtract. Perfect for lighting effects, shadows, or artistic styling.
* Dropdown selector in the sprite viewer menu
* Works alongside opacity controls
### <20>🌀 Rotation Wobble
Sprites can now do a subtle (or chaotic) rotational wobble. Frequency + amplitude sliders included.
![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.
@@ -29,6 +48,25 @@ 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)
### ↗️ Editor Middle Mouse Button Panning
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)
### ↗️ 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
Merged in [k0ffinzs](https://github.com/k0ffinz/PNGTuber-Plus) audio tweaks:
@@ -46,6 +84,7 @@ Updated to **Godot 4.4.1** — no more editor nags or compatibility weirdness.
### 🎚️ StreamDeck Integration
StreamDeck plugin was already in, this just wires up the buttons.
---
## 🧾 Credits

View File

@@ -4,4 +4,4 @@ name="Godot Git Plugin"
description="This plugin lets you interact with Git without leaving the Godot editor. More information can be found at https://github.com/godotengine/godot-git-plugin/wiki"
author="twaritwaikar"
version="v3.1.0"
script="godot-git-plugin.gd"
script=""

View File

@@ -0,0 +1,58 @@
extends Node
## Blend Mode Manager
##
## Manages blend mode constants and provides utility functions for
## working with blend modes in the PNGTuber application.
## Blend mode constants
enum BlendMode {
NORMAL = 0,
MULTIPLY = 1,
SCREEN = 2,
OVERLAY = 3,
SOFT_LIGHT = 4,
HARD_LIGHT = 5,
COLOR_DODGE = 6,
COLOR_BURN = 7,
DARKEN = 8,
LIGHTEN = 9,
ADD = 10, # Linear Dodge
SUBTRACT = 11
}
## Returns an array of blend mode names for use in dropdowns
static func get_blend_mode_names() -> Array:
return [
"Normal",
"Multiply",
"Screen",
"Overlay",
"Soft Light",
"Hard Light",
"Color Dodge",
"Color Burn",
"Darken",
"Lighten",
"Add",
"Subtract"
]
## Returns the string name for a given blend mode enum value
static func get_blend_mode_name(mode: BlendMode) -> String:
var names = get_blend_mode_names()
if mode >= 0 and mode < names.size():
return names[mode]
return "Normal"
## Returns the blend mode enum value for a given name
static func get_blend_mode_from_name(name: String) -> BlendMode:
var names = get_blend_mode_names()
var index = names.find(name)
if index != -1:
return index as BlendMode
return BlendMode.NORMAL
## Returns whether a blend mode is valid
static func is_valid_blend_mode(mode: int) -> bool:
return mode >= BlendMode.NORMAL and mode <= BlendMode.SUBTRACT

View File

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

View File

@@ -106,7 +106,7 @@ func _process(delta):
heldSprite.z += 1
heldSprite.setZIndex()
pushUpdate("Moved sprite layer.")
if main.editMode:
if main and main.editMode:
if Input.is_action_just_pressed("reparent"):
reparentMode = !reparentMode
Global.chain.enable(reparentMode)
@@ -115,7 +115,7 @@ func _process(delta):
reparentMode = false
Global.chain.enable(reparentMode)
if main.editMode:
if main and main.editMode:
if reparentMode:
RenderingServer.set_default_clear_color(Color.POWDER_BLUE)
else:
@@ -126,7 +126,7 @@ func _process(delta):
scrollSprites()
if !main.fileSystemOpen:
if !main or main.fileSystemOpen:
if Input.is_action_just_pressed("refresh"):
refresh()
@@ -222,7 +222,7 @@ func scrollSprites():
if Input.is_action_pressed("control"):
return
if !main.editMode:
if !main or !main.editMode:
return
if main.fileSystemOpen:

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

View File

@@ -4,7 +4,8 @@ extends Node2D
@onready var editButton = $Edit/Button
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()):
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:
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:
continue
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"):
sprites[s].scale = Vector2(0.6,0.6)
match b:
@@ -50,7 +51,8 @@ func _process(delta):
Global.mouse.text = "Duplicate sprite"
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
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 bounceGravity = 1000
# Physics timing multiplier - adjust this to fine-tune physics speed
# 1.0 = very slow, 10.0 = reasonable, 60.0 = old frame-based speed
var physicsTimeMultiplier = 6.0
#Costumes
var costume = 1
var bounceOnCostumeChange = false
@@ -43,6 +47,18 @@ var bounceOnCostumeChange = false
#Zooming
var scaleOverall = 100
#Camera Panning
var isPanning = false
var initialPanPosition = Vector2.ZERO
var initialCameraOffset = Vector2.ZERO
var cameraOffset = Vector2.ZERO
#View Mode Panning
var viewModePanning = false
var initialViewPanPosition = Vector2.ZERO
var viewPanOffset = Vector2.ZERO
var viewPanLerpSpeed = 3.0
var bounceChange = 0.0
#IMPORTANT
@@ -62,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"]
@@ -130,28 +159,93 @@ func _ready():
var s = get_viewport().get_visible_rect().size
origin.position = s*0.5
camera.position = origin.position
updateCameraPosition()
func _process(delta):
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:
origin.get_parent().position.y = 0
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"):
OS.shell_open(ProjectSettings.globalize_path("user://"))
moveSpriteMenu(delta)
zoomScene()
zoomScene(delta)
fileSystemOpen = isFileSystemOpen()
followShadow()
## Handle input events, especially for camera panning
func _input(event):
# Handle middle mouse button for camera panning/reset
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE:
if editMode:
if event.pressed:
# Start panning in edit mode
isPanning = true
initialPanPosition = event.global_position
initialCameraOffset = cameraOffset
else:
# Stop panning
isPanning = false
else:
if event.pressed:
# Start view mode panning
viewModePanning = true
initialViewPanPosition = event.global_position
else:
# Stop view mode panning
viewModePanning = false
# Handle mouse movement during edit mode panning
elif editMode and event is InputEventMouseMotion and isPanning:
var totalDelta = event.global_position - initialPanPosition
# Scale pan distance based on zoom level for more intuitive panning
# Higher zoom (closer) = less movement, lower zoom (farther) = more movement
var zoomScale = 1.0 / camera.zoom.x
var scaledDelta = totalDelta * zoomScale
# Calculate new camera offset from initial position plus scaled movement
cameraOffset = initialCameraOffset - scaledDelta
# Update camera position immediately
var s = get_viewport().get_visible_rect().size
camera.position = origin.position + cameraOffset
# Update UI positions
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
tutorial.position = controlPanel.position
editControls.position = camera.position - (s/(camera.zoom*2.0))
viewerArrows.position = editControls.position
pushUpdates.position.y = controlPanel.position.y
pushUpdates.position.x = editControls.position.x
# Handle mouse movement during view mode panning
elif !editMode and event is InputEventMouseMotion and viewModePanning:
var totalDelta = event.global_position - initialViewPanPosition
# Scale pan distance for view mode (less sensitive than edit mode)
# Invert the movement so dragging right moves character right (not left)
var panSensitivity = 0.5
viewPanOffset = -totalDelta * panSensitivity
func followShadow():
# Shadow disabled for selected sprites
shadow.visible = false
@@ -164,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
@@ -189,16 +288,10 @@ func onWindowSizeChange():
lines.position = s*0.5
lines.drawLine()
camera.position = origin.position
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
tutorial.position = controlPanel.position
editControls.position = camera.position - (s/(camera.zoom*2.0))
viewerArrows.position = editControls.position
updateCameraPosition()
spriteList.position.x = s.x - 233
pushUpdates.position.y = controlPanel.position.y
pushUpdates.position.x = editControls.position.x
func zoomScene():
func zoomScene(delta):
#Handles Zooming
if Input.is_action_pressed("control"):
if Input.is_action_just_pressed("scrollUp"):
@@ -212,7 +305,7 @@ func zoomScene():
scaleOverall -= 10
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():
var newZoom = Vector2(1.0,1.0) / camera.zoom
@@ -224,11 +317,45 @@ func changeZoom():
pushUpdates.scale = newZoom
Global.mouse.scale = newZoom
# Update UI positions to account for new zoom level
# Force update even during panning since zoom affects UI positioning calculations
var s = get_viewport().get_visible_rect().size
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
tutorial.position = controlPanel.position
editControls.position = camera.position - (s/(camera.zoom*2.0))
viewerArrows.position = editControls.position
pushUpdates.position.y = controlPanel.position.y
pushUpdates.position.x = editControls.position.x
$ControlPanel/ZoomLabel.modulate.a = 6.0
$ControlPanel/ZoomLabel.text = "Zoom : " + str(scaleOverall) + "%"
Global.pushUpdate("Set zoom to " + str(scaleOverall) + "%")
onWindowSizeChange()
## Update camera position with current offset
func updateCameraPosition():
var s = get_viewport().get_visible_rect().size
# Only update if not currently panning to avoid conflicts
if !isPanning:
camera.position = origin.position + cameraOffset
# Update UI element positions relative to new camera position
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
tutorial.position = controlPanel.position
editControls.position = camera.position - (s/(camera.zoom*2.0))
viewerArrows.position = editControls.position
pushUpdates.position.y = controlPanel.position.y
pushUpdates.position.x = editControls.position.x
## Reset camera position to center
func resetCameraPosition():
cameraOffset = Vector2.ZERO
isPanning = false
viewPanOffset = Vector2.ZERO
viewModePanning = false
updateCameraPosition()
Global.pushUpdate("Camera reset to center.")
#When the user speaks!
func onSpeak():
@@ -243,6 +370,14 @@ func swapMode():
editMode = !editMode
Global.pushUpdate("Toggled editing mode.")
# Reset camera when entering view mode
if !editMode:
resetCameraPosition()
else:
# Reset view mode panning when entering edit mode
viewPanOffset = Vector2.ZERO
viewModePanning = false
get_viewport().transparent_bg = !editMode
if Global.backgroundColor.a != 0.0:
get_viewport().transparent_bg = false
@@ -290,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"]
@@ -357,6 +527,12 @@ func _on_load_dialog_file_selected(path):
sprite.spriteOpacity = data[item]["spriteOpacity"]
if data[item].has("affectChildrenOpacity"):
sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"]
if data[item].has("blendMode"):
sprite.blendMode = data[item]["blendMode"]
# Load wobble sync group (with backward compatibility)
if data[item].has("wobbleSyncGroup"):
sprite.wobbleSyncGroup = data[item]["wobbleSyncGroup"]
origin.add_child(sprite)
sprite.position = str_to_var(data[item]["pos"])
@@ -365,6 +541,49 @@ func _on_load_dialog_file_selected(path):
Saving.settings["lastAvatar"] = path
Global.spriteList.updateData()
# 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:
if sprite.has_method("updateOpacity"):
sprite.updateOpacity()
Global.pushUpdate("Loaded avatar at: " + path)
onWindowSizeChange()
@@ -421,9 +640,17 @@ func _on_save_dialog_file_selected(path):
data[id]["spriteOpacity"] = child.spriteOpacity
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
data[id]["blendMode"] = child.blendMode
# 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()
@@ -500,10 +727,18 @@ func _on_duplicate_button_pressed():
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
sprite.blendMode = Global.heldSprite.blendMode
# Copy wobble sync group
sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup
origin.add_child(sprite)
sprite.position = Global.heldSprite.position + Vector2(16,16)
# Update opacity shader for the duplicated sprite
await get_tree().process_frame # Wait for sprite to be fully initialized
sprite.updateOpacity()
Global.heldSprite = sprite
Global.spriteList.updateData()
@@ -548,7 +783,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
@@ -576,9 +811,9 @@ func moveSpriteMenu(delta):
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):
Global.spriteEdit.position.y -= (delta*432.0)
Global.spriteEdit.position.y -= (delta * 400.0)
if Global.spriteEdit.position.y > 66:
Global.spriteEdit.position.y = 66

View File

@@ -14358,6 +14358,7 @@ text = "Zoom : 100%"
horizontal_alignment = 2
[node name="VersionLabels" type="Node2D" parent="ControlPanel"]
position = Vector2(-284, 59)
[node name="Label" type="Label" parent="ControlPanel/VersionLabels"]
offset_left = -713.0
@@ -14377,17 +14378,39 @@ label_settings = SubResource("LabelSettings_qg0do")
[node name="versionNo" type="Label" parent="ControlPanel/VersionLabels/Label2"]
layout_mode = 0
offset_left = 144.0
offset_top = 40.0
offset_right = 282.0
offset_bottom = 74.0
offset_left = 104.0
offset_top = 11.0
offset_right = 242.0
offset_bottom = 54.0
text = "v1.4.5"
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="."]
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 = 273.485
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")
@@ -14549,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
@@ -14610,7 +14628,6 @@ patch_margin_right = 8
patch_margin_bottom = 8
[node name="Label" type="Label" parent="Tutorial/NinePatchRect"]
visible = false
layout_mode = 0
offset_left = 15.0
offset_top = 14.0

View File

@@ -29,6 +29,8 @@ buses/channel_disable_time=0.0
Saving="*res://autoload/saving.gd"
Global="*res://autoload/global.gd"
WobbleSyncManager="*res://autoload/wobble_sync_manager.gd"
BlendModeManager="*res://autoload/blend_mode_manager.gd"
ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd"
DefaultAvatarData="*res://autoload/defaultAvatarData.gd"
@@ -50,8 +52,8 @@ project/assembly_name="PNGTuberPlus"
[editor]
version_control/plugin_name="GitPlugin"
version_control/autoload_on_startup=true
version_control/plugin_name=""
version_control/autoload_on_startup=false
[editor_plugins]
@@ -136,6 +138,11 @@ control={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
mouse_middle={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
saveImages={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":108,"location":0,"echo":false,"script":null)

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

@@ -0,0 +1,82 @@
## Example implementation showing how to replace legacy wobble controls
## with the new WobbleControlPanel component
extends Node2D
## This demonstrates the integration approach for the sprite viewer
## Legacy slider references (to be replaced)
@onready var legacy_wobble_control = $LegacyWobbleControl
## New component system
@onready var wobble_panel: WobbleControlPanel = $WobbleControlPanel
var slider_manager: SliderManager
func _ready():
# Initialize slider management
slider_manager = SliderManager.new(self)
# Set up the wobble panel
if wobble_panel:
# The panel will automatically configure its sliders
print("WobbleControlPanel initialized")
# Hide legacy controls when using new system
if legacy_wobble_control:
legacy_wobble_control.visible = false
## Updated setImage function using new components
func setImage_new_system():
if Global.heldSprite == null:
return
print("Setting image with new component system")
# Update wobble panel with current sprite
if wobble_panel:
wobble_panel.set_target_sprite(Global.heldSprite)
wobble_panel.update_from_sprite()
# Update sync feedback
updateWobbleControlStates_new_system()
## Updated wobble control states using new system
func updateWobbleControlStates_new_system():
if Global.heldSprite == null:
return
if wobble_panel:
wobble_panel.update_sync_feedback()
## Migration helper - gradually replace sections
func migrate_to_new_system():
print("Migrating wobble controls to new component system...")
# Step 1: Hide old controls
if has_node("WobbleControl"):
$WobbleControl.visible = false
# Step 2: Show new panel
if wobble_panel:
wobble_panel.visible = true
# Step 3: Update with current sprite
if Global.heldSprite:
setImage_new_system()
print("Migration complete!")
## Demonstration of component benefits
func demonstrate_consistency():
print("=== Component System Benefits ===")
if wobble_panel:
print("All wobble sliders have consistent:")
print("- Visual styling")
print("- Value formatting")
print("- Label positioning")
print("- Signal handling")
print("- Sync state feedback")
print("Components are easily reusable across scenes")
print("Centralized management through SliderManager")
print("Type-safe with proper class inheritance")

View File

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

View File

@@ -0,0 +1,35 @@
[gd_scene load_steps=3 format=4 uid="uid://cm4n7yap2k8q1"]
[ext_resource type="Script" path="res://ui_scenes/components/ComponentMigrationExample.gd" id="1_migration_script"]
[ext_resource type="PackedScene" uid="uid://bp2q8r4l6k9m3" path="res://ui_scenes/components/WobbleControlPanel.tscn" id="2_wobble_panel"]
[node name="ComponentMigrationExample" type="Node2D"]
script = ExtResource("1_migration_script")
[node name="Background" type="ColorRect" parent="."]
offset_right = 300.0
offset_bottom = 400.0
color = Color(0.2, 0.2, 0.2, 1)
[node name="Title" type="Label" parent="."]
offset_left = 10.0
offset_top = 10.0
offset_right = 290.0
offset_bottom = 35.0
text = "New Component System Demo"
horizontal_alignment = 1
[node name="WobbleControlPanel" parent="." instance=ExtResource("2_wobble_panel")]
offset_left = 10.0
offset_top = 50.0
offset_right = 290.0
offset_bottom = 350.0
[node name="Instructions" type="Label" parent="."]
offset_left = 10.0
offset_top = 360.0
offset_right = 290.0
offset_bottom = 390.0
text = "These sliders automatically sync with sprites"
horizontal_alignment = 1
vertical_alignment = 1

View File

@@ -0,0 +1,105 @@
# Implementation Complete: Sprite Viewer Slider Components
## ✅ **Successfully Implemented**
### **🎯 Components Created**
- **LabeledSlider** - Base reusable slider component
- **WobbleSlider** - Specialized for wobble parameters with sync support
- **ParameterSlider** - Generic parameter binding for sprite properties
- **SliderManager** - Centralized management system
### **🔄 Replaced in sprite_viewer.tscn**
| **Old Slider** | **New Component** | **Benefits** |
|----------------|-------------------|--------------|
| Drag Slider | ParameterSlider | Auto label updates, consistent formatting |
| X Frequency | WobbleSlider | Sync group support, auto parameter binding |
| X Amplitude | WobbleSlider | Consistent wobble handling |
| Y Frequency | WobbleSlider | Integrated with wobble sync system |
| Y Amplitude | WobbleSlider | Visual feedback for sync status |
| Rotation Frequency | WobbleSlider | Proper Hz display formatting |
| Rotation Amplitude | WobbleSlider | Percentage display with % suffix |
| Rotation Drag | ParameterSlider | Simplified parameter binding |
| Squash | ParameterSlider | Decimal precision formatting |
| Rotation Limit Min | ParameterSlider | Degree (°) suffix display |
| Rotation Limit Max | ParameterSlider | Consistent limit handling |
| Animation Frames | ParameterSlider | Integer-only values |
| Animation Speed | ParameterSlider | Decimal speed values |
| Opacity | ParameterSlider | Custom percentage display |
### **🎨 Key Features Implemented**
**Automatic Label Updates** - No more manual label text management
**Consistent Formatting** - All sliders use the same styling
**Parameter Binding** - Direct sprite property connections
**Wobble Sync Integration** - Visual feedback and group handling
**Type Safety** - Proper class inheritance
**Value Formatting** - Decimal places, suffixes (Hz, %, °)
**Signal Management** - Components handle their own connections
**Backwards Compatibility** - Old functions cleaned up
### **🔧 Technical Implementation**
#### **Sprite Viewer Updates:**
- Added SliderManager for centralized control
- Implemented `_setup_slider_components()`
- Updated `setImage()` to use component system
- Modified `updateWobbleControlStates()` for visual sync feedback
- Removed redundant old signal handlers
- Added new component signal connections
#### **Scene File Changes:**
- Added external resource references for new components
- Replaced 14 individual slider+label combinations
- Configured each component with appropriate parameters
- Removed outdated signal connections
- Optimized layout spacing
#### **Component Features:**
- **LabeledSlider**: Base functionality with export properties
- **WobbleSlider**: Inherits from LabeledSlider, adds wobble-specific logic
- **ParameterSlider**: Generic sprite property binding with special cases
- **SliderManager**: Bulk operations and consistent styling
### **🎯 Benefits Achieved**
1. **Consistency** - All sliders look and behave identically
2. **Maintainability** - One place to modify slider behavior
3. **Reusability** - Components work across different scenes
4. **Reduced Code** - Less repetitive slider setup and management
5. **Better UX** - Automatic updates, proper formatting, visual feedback
6. **Type Safety** - Class-based components with proper inheritance
### **📝 Usage Example**
**Before (Old System):**
```gdscript
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
$WobbleControl/xFrq.value = value
# Repeat for every slider...
```
**After (New Components):**
```gdscript
slider_manager.update_all_sliders(Global.heldSprite)
# All sliders update automatically with proper formatting
```
### **🔧 Migration Notes**
- All old individual sliders have been replaced
- Legacy signal handlers removed
- Component system handles parameter updates automatically
- Visual sync feedback integrated into wobble sliders
- Opacity slider has custom percentage display logic
### **🚀 Ready to Use**
The sprite viewer now uses a modern, component-based slider system that is:
- More maintainable
- More consistent
- Easier to extend
- Better organized
- Type-safe
All sliders in the sprite viewer are now implemented as reusable components! 🎉

View File

@@ -0,0 +1,73 @@
# 🔧 Fixed: Slider Labels Now Show Correct Text
## ✅ **Problem Solved**
The issue was that slider labels were showing "Value" instead of the actual `label_text` property (like "X Frequency", "Drag", etc.) in the Godot editor.
## 🎯 **Root Cause**
1. **Editor Execution**: Components needed `@tool` to work properly in the editor
2. **Initialization Timing**: Label updates happened before exported properties were set
3. **Property Setter Timing**: Label wasn't updated when properties were set by the editor
## 🔧 **Solution Applied**
### **1. Added `@tool` Directive**
Added `@tool` to all component scripts to enable editor execution:
- `LabeledSlider.gd`
- `WobbleSlider.gd`
- `ParameterSlider.gd`
### **2. Improved Property Setters**
Updated all export property setters to check if the node is ready:
```gdscript
func set_label_text(new_text: String):
label_text = new_text
if is_inside_tree():
_update_label()
else:
call_deferred("_update_label")
```
### **3. Enhanced Initialization**
- Added `NOTIFICATION_SCENE_INSTANTIATED` handler
- Improved `_ready()` function timing
- Added deferred label updates for proper timing
### **4. Fixed Base Template**
Updated `LabeledSlider.tscn` to have cleaner default text:
```
text = "Value" # Instead of "Value: 0"
```
### **5. Robust Label Updates**
Enhanced `_update_label()` to handle missing nodes gracefully:
```gdscript
func _update_label():
if not label:
label = get_node_or_null("Label")
if not label:
call_deferred("_update_label")
return
# ... update logic
```
## 🎉 **Result**
Now in the Godot editor, all slider labels should properly display:
- ✅ "X Frequency: 0.000 Hz"
- ✅ "Y Amplitude: 0"
- ✅ "Drag: 0.0"
- ✅ "Opacity: 100%"
- ✅ "Rotational Limit Min: -180°"
Instead of just "Value" everywhere!
## 🔄 **How to Test**
1. Open `sprite_viewer.tscn` in the Godot editor
2. Check the slider components in the scene tree
3. Labels should now show the correct text for each parameter
4. Properties can be modified in the inspector and labels update immediately
The components now work properly both in the editor preview and at runtime! 🚀

View File

@@ -0,0 +1,140 @@
@tool
## A reusable component that combines a label and slider
## Provides consistent formatting and behavior for all sliders in the UI
extends VBoxContainer
class_name LabeledSlider
## Emitted when the slider value changes
signal value_changed(value: float)
## The base text that appears in the label (without the value)
@export var label_text: String = "Value" : set = set_label_text
## The minimum value of the slider
@export var min_value: float = 0.0 : set = set_min_value
## The maximum value of the slider
@export var max_value: float = 100.0 : set = set_max_value
## The step value for the slider
@export var step: float = 1.0 : set = set_step
## The current value of the slider
@export var value: float = 0.0 : set = set_value
## Whether to show the value in the label
@export var show_value_in_label: bool = true : set = set_show_value_in_label
## Format string for the value display (e.g., "%.1f" for one decimal place)
@export var value_format: String = "%.0f" : set = set_value_format
## Optional suffix to add after the value (e.g., "%", "Hz")
@export var value_suffix: String = "" : set = set_value_suffix
## Whether the slider is editable
@export var editable: bool = true : set = set_editable
@onready var label: Label = $Label
@onready var slider: HSlider = $HSlider
func _ready():
if slider:
slider.value_changed.connect(_on_slider_value_changed)
_update_slider_properties()
# Ensure label is updated after all properties are set
call_deferred("_update_label")
func _notification(what):
if what == NOTIFICATION_SCENE_INSTANTIATED:
# Update label when scene is instantiated (important for editor)
call_deferred("_update_label")
## Set the label text
func set_label_text(new_text: String):
label_text = new_text
if is_inside_tree():
_update_label()
else:
call_deferred("_update_label")
## Set the minimum value
func set_min_value(new_min: float):
min_value = new_min
if is_inside_tree():
_update_slider_properties()
## Set the maximum value
func set_max_value(new_max: float):
max_value = new_max
if is_inside_tree():
_update_slider_properties()
## Set the step value
func set_step(new_step: float):
step = new_step
if is_inside_tree():
_update_slider_properties()
## Set the current value
func set_value(new_value: float):
value = new_value
if slider:
slider.value = value
if is_inside_tree():
_update_label()
## Set whether to show value in label
func set_show_value_in_label(show: bool):
show_value_in_label = show
if is_inside_tree():
_update_label()
## Set the value format string
func set_value_format(format: String):
value_format = format
if is_inside_tree():
_update_label()
## Set the value suffix
func set_value_suffix(suffix: String):
value_suffix = suffix
if is_inside_tree():
_update_label()
## Set whether the slider is editable
func set_editable(is_editable: bool):
editable = is_editable
if slider:
slider.editable = editable
## Update the slider properties
func _update_slider_properties():
if not slider:
return
slider.min_value = min_value
slider.max_value = max_value
slider.step = step
slider.value = value
slider.editable = editable
## Update the label text
func _update_label():
if not label:
# If label isn't ready yet, try to find it
label = get_node_or_null("Label")
if not label:
# Call again after a frame when nodes are ready
call_deferred("_update_label")
return
var text = label_text
if show_value_in_label:
var formatted_value = value_format % value
text += ": " + formatted_value + value_suffix
label.text = text
## Handle slider value changes
func _on_slider_value_changed(new_value: float):
value = new_value
_update_label()
value_changed.emit(value)
## Set the modulate color for visual feedback
func set_modulate_color(color: Color):
if slider:
slider.modulate = color

View File

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

View File

@@ -0,0 +1,22 @@
[gd_scene load_steps=2 format=3 uid="uid://bxm5q6n8x7p2q"]
[ext_resource type="Script" uid="uid://combq5rrok31i" path="res://ui_scenes/components/LabeledSlider.gd" id="1_labeled_slider"]
[node name="LabeledSlider" type="VBoxContainer"]
offset_right = 200.0
offset_bottom = 40.0
size_flags_horizontal = 3
size_flags_vertical = 0
theme_override_constants/separation = 2
script = ExtResource("1_labeled_slider")
[node name="Label" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "Value: 0"
vertical_alignment = 1
[node name="HSlider" type="HSlider" parent="."]
layout_mode = 2
size_flags_horizontal = 3

View File

@@ -0,0 +1,82 @@
@tool
## A parameter slider component for general sprite parameters
## Handles updating sprite properties with proper formatting
extends LabeledSlider
class_name ParameterSlider
## The sprite parameter name this slider controls
@export var parameter_name: String = ""
## The property path in the sprite object (e.g., "dragSpeed", "rdragStr")
@export var sprite_property: String = ""
## Reference to the sprite being edited
var target_sprite = null
func _ready():
super._ready()
# Connect to value changes to update parameters
value_changed.connect(_on_parameter_value_changed)
func _notification(what):
super._notification(what)
if what == NOTIFICATION_SCENE_INSTANTIATED:
# Update label for opacity specially
if sprite_property == "spriteOpacity":
call_deferred("_update_opacity_label")
## Set the sprite this slider controls
func set_target_sprite(sprite):
target_sprite = sprite
if sprite:
_update_from_sprite()
## Update the slider from the sprite's current values
func _update_from_sprite():
if not target_sprite or sprite_property == "":
return
var current_value = target_sprite.get(sprite_property)
if current_value != null:
set_value(current_value)
# Special handling for opacity display
if sprite_property == "spriteOpacity":
_update_opacity_label()
## Update label text with special opacity handling
func _update_opacity_label():
if sprite_property == "spriteOpacity" and label:
var percentage = int(value * 100)
label.text = label_text + ": " + str(percentage) + "%"
## Override base update_label for special cases
func _update_label():
if sprite_property == "spriteOpacity":
_update_opacity_label()
else:
super._update_label()
## Handle parameter changes
func _on_parameter_value_changed(new_value: float):
if not target_sprite or sprite_property == "":
return
print("DEBUG: ParameterSlider ", sprite_property, " changed to: ", new_value)
target_sprite.set(sprite_property, new_value)
# Call specific update methods if they exist
_call_update_method()
## Call specific update methods for certain parameters
func _call_update_method():
if not target_sprite:
return
match sprite_property:
"spriteOpacity":
target_sprite.updateOpacity()
"frames":
target_sprite.changeFrames()
"blendMode":
target_sprite.updateBlendMode(value)

View File

@@ -0,0 +1 @@
uid://6maee7dhl7vl

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=3 format=3 uid="uid://fgee1pq0yqav"]
[ext_resource type="PackedScene" uid="uid://bxm5q6n8x7p2q" path="res://ui_scenes/components/LabeledSlider.tscn" id="1_base_slider"]
[ext_resource type="Script" uid="uid://6maee7dhl7vl" path="res://ui_scenes/components/ParameterSlider.gd" id="2_parameter_script"]
[node name="ParameterSlider" instance=ExtResource("1_base_slider")]
script = ExtResource("2_parameter_script")
parameter_name = ""
sprite_property = ""

View File

@@ -0,0 +1,220 @@
# Slider Components Documentation
This document explains the new slider component system that replaces the individual sliders in the sprite viewer with reusable, consistent components.
## Components Overview
### 1. LabeledSlider (Base Component)
**File:** `ui_scenes/components/LabeledSlider.tscn`
**Script:** `ui_scenes/components/LabeledSlider.gd`
The base slider component that combines a label and slider into one reusable unit.
**Key Features:**
- Automatic label updates with current value
- Configurable value formatting
- Optional value suffix (%, Hz, etc.)
- Consistent styling
- Signal emission on value changes
**Properties:**
- `label_text`: Base text for the label
- `min_value`, `max_value`, `step`: Slider configuration
- `value`: Current value
- `show_value_in_label`: Whether to display value in label
- `value_format`: Format string for value display (e.g., "%.1f")
- `value_suffix`: Suffix to add after value (e.g., "%", "Hz")
- `editable`: Whether the slider can be modified
### 2. WobbleSlider (Specialized Component)
**File:** `ui_scenes/components/WobbleSlider.tscn`
**Script:** `ui_scenes/components/WobbleSlider.gd`
Extends LabeledSlider specifically for wobble parameters.
**Key Features:**
- Automatic synchronization with sprite wobble parameters
- Visual feedback for sync group status
- Integrated with wobble sync system
- Parameter-specific configuration
**Properties:**
- `parameter_name`: The wobble parameter this controls ("xFrq", "xAmp", etc.)
- All LabeledSlider properties
### 3. ParameterSlider (Generic Component)
**File:** `ui_scenes/components/ParameterSlider.tscn`
**Script:** `ui_scenes/components/ParameterSlider.gd`
Extends LabeledSlider for general sprite parameters.
**Key Features:**
- Generic parameter binding to sprite properties
- Automatic update method calling for specific parameters
- Property path configuration
**Properties:**
- `sprite_property`: The sprite property to bind to
- `parameter_name`: Display name for the parameter
- All LabeledSlider properties
### 4. WobbleControlPanel (Composite Component)
**File:** `ui_scenes/components/WobbleControlPanel.tscn`
**Script:** `ui_scenes/components/WobbleControlPanel.gd`
A complete panel containing all wobble sliders.
**Key Features:**
- Pre-configured wobble sliders
- Centralized sprite management
- Integrated sync feedback
- Easy integration with existing wobble sync controls
### 5. SliderManager (Management System)
**Script:** `ui_scenes/components/SliderManager.gd`
Manages collections of slider components.
**Key Features:**
- Centralized slider updates
- Consistent styling application
- Bulk operations on sliders
- Integration with sprite viewer
## Usage Examples
### Basic LabeledSlider
```gdscript
# In your scene
@onready var my_slider: LabeledSlider = $LabeledSlider
func _ready():
my_slider.label_text = "Volume"
my_slider.min_value = 0.0
my_slider.max_value = 100.0
my_slider.value_suffix = "%"
my_slider.value_changed.connect(_on_volume_changed)
func _on_volume_changed(value: float):
print("Volume changed to: ", value)
```
### WobbleSlider Configuration
```gdscript
# In scene setup
@onready var x_freq_slider: WobbleSlider = $XFrequencySlider
func _ready():
x_freq_slider.label_text = "X Frequency"
x_freq_slider.parameter_name = "xFrq"
x_freq_slider.min_value = 0.0
x_freq_slider.max_value = 10.0
x_freq_slider.step = 0.1
x_freq_slider.value_format = "%.1f"
x_freq_slider.value_suffix = " Hz"
func set_sprite(sprite):
x_freq_slider.set_target_sprite(sprite)
```
### Using WobbleControlPanel
```gdscript
# Replace individual wobble sliders with the panel
@onready var wobble_panel: WobbleControlPanel = $WobbleControlPanel
func setImage():
if Global.heldSprite:
wobble_panel.set_target_sprite(Global.heldSprite)
wobble_panel.update_from_sprite()
```
### SliderManager Integration
```gdscript
# In sprite viewer
var slider_manager: SliderManager
func _ready():
slider_manager = SliderManager.new(self)
_setup_sliders()
func _setup_sliders():
# Add sliders to manager
slider_manager.add_slider($DragSlider)
slider_manager.add_slider($OpacitySlider)
# etc.
func setImage():
if slider_manager:
slider_manager.update_all_sliders(Global.heldSprite)
```
## Migration from Legacy Sliders
### Step 1: Replace Scene Nodes
Replace individual HSlider + Label combinations with LabeledSlider instances:
**Before:**
```
- Node2D
- Label (for "x frequency: 5.0")
- HSlider (for value input)
```
**After:**
```
- Node2D
- LabeledSlider (handles both label and slider)
```
### Step 2: Update Script References
**Before:**
```gdscript
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
$WobbleControl/xFrq.value = value
```
**After:**
```gdscript
$WobbleControl/XFrequencySlider.set_value(value)
# Label updates automatically
```
### Step 3: Connect Signals
**Before:**
```gdscript
func _on_x_frq_value_changed(value):
Global.heldSprite.xFrq = value
```
**After:**
```gdscript
# Handled automatically by WobbleSlider component
# Or connect to the component's value_changed signal
```
## Benefits
1. **Consistency**: All sliders look and behave the same way
2. **Maintainability**: Changes to slider behavior only need to be made in one place
3. **Reusability**: Components can be used across different parts of the application
4. **Type Safety**: Proper class inheritance with specific functionality
5. **Reduced Code**: Less repetitive slider setup code
6. **Better Organization**: Clear separation of concerns
## Integration Notes
- The new system is designed to be backwards compatible
- Legacy slider code is maintained as fallback
- Components can be gradually adopted
- SliderManager provides centralized control
- Easy to extend for new parameter types
## Custom Styling
To apply consistent styling across all sliders, modify the base LabeledSlider component or use the SliderManager's styling methods:
```gdscript
func apply_custom_theme():
for slider in slider_manager.sliders:
slider.slider.add_theme_stylebox_override("slider", custom_style)
```

View File

@@ -0,0 +1,47 @@
[gd_scene load_steps=4 format=4 uid="uid://bh7n2k1q5m8ya"]
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="1_wobble_x_frq"]
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="2_wobble_x_amp"]
[ext_resource type="PackedScene" uid="uid://d9t8y3m0k6l1n" path="res://ui_scenes/components/ParameterSlider.tscn" id="3_drag_slider"]
[node name="SampleWobbleControl" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -1680.0
offset_bottom = -1080.0
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="XFrequencySlider" parent="VBoxContainer" instance=ExtResource("1_wobble_x_frq")]
layout_mode = 2
label_text = "X Frequency"
min_value = 0.0
max_value = 10.0
step = 0.1
value_format = "%.1f"
value_suffix = " Hz"
parameter_name = "xFrq"
[node name="XAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_x_amp")]
layout_mode = 2
label_text = "X Amplitude"
min_value = 0.0
max_value = 100.0
step = 1.0
value_format = "%.0f"
parameter_name = "xAmp"
[node name="DragSlider" parent="VBoxContainer" instance=ExtResource("3_drag_slider")]
layout_mode = 2
label_text = "Drag"
min_value = 0.0
max_value = 10.0
step = 0.1
value_format = "%.1f"
sprite_property = "dragSpeed"

View File

View File

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

View File

@@ -0,0 +1,60 @@
## Manages all slider components in the sprite viewer
## Provides a centralized way to update and configure sliders
extends RefCounted
class_name SliderManager
## Array of all slider components
var sliders: Array[LabeledSlider] = []
## Reference to the sprite viewer
var sprite_viewer = null
func _init(viewer):
sprite_viewer = viewer
## Add a slider to be managed
func add_slider(slider: LabeledSlider):
if slider not in sliders:
sliders.append(slider)
## Remove a slider from management
func remove_slider(slider: LabeledSlider):
sliders.erase(slider)
## Update all sliders with the current sprite data
func update_all_sliders(sprite):
for slider in sliders:
if slider is WobbleSlider:
slider.set_target_sprite(sprite)
elif slider is ParameterSlider:
slider.set_target_sprite(sprite)
## Update wobble control states based on sync status
func update_wobble_control_states(sprite):
if not sprite:
return
var is_synced = sprite.isSynced()
for slider in sliders:
if slider is WobbleSlider:
# Keep wobble controls enabled even when synced (they edit group settings)
slider.set_editable(true)
# Visual feedback for sync state - slight tint to indicate group editing
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
slider.set_modulate_color(tint)
## Get a slider by its parameter name
func get_slider_by_parameter(parameter_name: String) -> LabeledSlider:
for slider in sliders:
if slider is WobbleSlider and slider.parameter_name == parameter_name:
return slider
elif slider is ParameterSlider and slider.sprite_property == parameter_name:
return slider
return null
## Apply consistent styling to all sliders
func apply_consistent_styling():
for slider in sliders:
# Apply any consistent styling here
pass

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
## A complete wobble control panel using the new slider components
## Provides a consistent interface for all wobble parameters
extends Control
class_name WobbleControlPanel
## Reference to all wobble sliders
@onready var x_frequency_slider: WobbleSlider = $VBoxContainer/XFrequencySlider
@onready var x_amplitude_slider: WobbleSlider = $VBoxContainer/XAmplitudeSlider
@onready var y_frequency_slider: WobbleSlider = $VBoxContainer/YFrequencySlider
@onready var y_amplitude_slider: WobbleSlider = $VBoxContainer/YAmplitudeSlider
@onready var r_frequency_slider: WobbleSlider = $VBoxContainer/RFrequencySlider
@onready var r_amplitude_slider: WobbleSlider = $VBoxContainer/RAmplitudeSlider
## Array of all wobble sliders for easy iteration
var wobble_sliders: Array[WobbleSlider] = []
## The sprite currently being edited
var target_sprite = null
func _ready():
# Collect all wobble sliders
wobble_sliders = [
x_frequency_slider,
x_amplitude_slider,
y_frequency_slider,
y_amplitude_slider,
r_frequency_slider,
r_amplitude_slider
]
# Connect to wobble sync control if it exists
var wobble_sync_control = find_child("WobbleSyncControl")
if wobble_sync_control:
_connect_wobble_sync_signals(wobble_sync_control)
## Set the target sprite for all sliders
func set_target_sprite(sprite):
target_sprite = sprite
for slider in wobble_sliders:
slider.set_target_sprite(sprite)
## Update all sliders from the current sprite
func update_from_sprite():
if target_sprite:
for slider in wobble_sliders:
slider._update_from_sprite()
## Update visual feedback for sync status
func update_sync_feedback():
for slider in wobble_sliders:
slider._update_sync_feedback()
## Connect signals from wobble sync control
func _connect_wobble_sync_signals(wobble_sync_control):
# Connect value change signals to update wobble sync control
for slider in wobble_sliders:
slider.value_changed.connect(_on_wobble_parameter_changed)
## Handle wobble parameter changes to update sync control
func _on_wobble_parameter_changed(value: float):
var wobble_sync_control = find_child("WobbleSyncControl")
if wobble_sync_control and wobble_sync_control.has_method("updateUI"):
wobble_sync_control.updateUI()

View File

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

View File

@@ -0,0 +1,75 @@
[gd_scene load_steps=8 format=4 uid="uid://bp2q8r4l6lam3"]
[ext_resource type="Script" path="res://ui_scenes/components/WobbleControlPanel.gd" id="1_wobble_panel_script"]
[ext_resource type="PackedScene" uid="uid://c8q7x2n9p4r5s" path="res://ui_scenes/components/WobbleSlider.tscn" id="2_wobble_slider"]
[node name="WobbleControlPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_wobble_panel_script")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="XFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "X Frequency"
min_value = 0.0
max_value = 10.0
step = 0.1
value_format = "%.1f"
value_suffix = " Hz"
parameter_name = "xFrq"
[node name="XAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "X Amplitude"
min_value = 0.0
max_value = 100.0
step = 1.0
value_format = "%.0f"
parameter_name = "xAmp"
[node name="YFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "Y Frequency"
min_value = 0.0
max_value = 10.0
step = 0.1
value_format = "%.1f"
value_suffix = " Hz"
parameter_name = "yFrq"
[node name="YAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "Y Amplitude"
min_value = 0.0
max_value = 100.0
step = 1.0
value_format = "%.0f"
parameter_name = "yAmp"
[node name="RFrequencySlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "Rotation Frequency"
min_value = 0.0
max_value = 10.0
step = 0.1
value_format = "%.1f"
value_suffix = " Hz"
parameter_name = "rFrq"
[node name="RAmplitudeSlider" parent="VBoxContainer" instance=ExtResource("2_wobble_slider")]
layout_mode = 2
label_text = "Rotation Amplitude"
min_value = 0.0
max_value = 100.0
step = 1.0
value_format = "%.0f"
value_suffix = "%"
parameter_name = "rAmp"

View File

@@ -0,0 +1,61 @@
@tool
## A specialized slider component for wobble parameters
## Automatically handles syncing with wobble groups and provides appropriate feedback
extends LabeledSlider
class_name WobbleSlider
## The wobble parameter name this slider controls
@export var parameter_name: String = ""
## Reference to the sprite being edited
var target_sprite = null
func _ready():
super._ready()
# Connect to value changes to update wobble parameters
value_changed.connect(_on_wobble_value_changed)
## Set the sprite this slider controls
func set_target_sprite(sprite):
target_sprite = sprite
if sprite:
_update_from_sprite()
## Update the slider from the sprite's current values
func _update_from_sprite():
if not target_sprite or parameter_name == "":
return
match parameter_name:
"xFrq":
set_value(target_sprite.xFrq)
"xAmp":
set_value(target_sprite.xAmp)
"yFrq":
set_value(target_sprite.yFrq)
"yAmp":
set_value(target_sprite.yAmp)
"rFrq":
set_value(target_sprite.rFrq)
"rAmp":
set_value(target_sprite.rAmp)
## Handle wobble parameter changes
func _on_wobble_value_changed(new_value: float):
if not target_sprite or parameter_name == "":
return
print("DEBUG: WobbleSlider ", parameter_name, " changed to: ", new_value)
target_sprite.updateWobbleParameter(parameter_name, new_value)
# Update visual feedback for sync status
_update_sync_feedback()
## Update visual feedback based on sync status
func _update_sync_feedback():
if not target_sprite:
return
var is_synced = target_sprite.isSynced()
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
set_modulate_color(tint)

View File

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

View File

@@ -0,0 +1,19 @@
[gd_scene load_steps=3 format=3 uid="uid://c8q7x2oap4r5s"]
[ext_resource type="PackedScene" uid="uid://bxm5q6n8x7p2q" path="res://ui_scenes/components/LabeledSlider.tscn" id="1_base_slider"]
[ext_resource type="Script" uid="uid://fsc2h7ajjcbs" path="res://ui_scenes/components/WobbleSlider.gd" id="2_wobble_script"]
[node name="WobbleSlider" instance=ExtResource("1_base_slider")]
script = ExtResource("2_wobble_script")
parameter_name = ""
[node name="Label" parent="." index="0"]
anchors_preset = 10
grow_horizontal = 2
text = "Value: 0"
horizontal_alignment = 0
[node name="HSlider" parent="." index="1"]
anchors_preset = 12
grow_horizontal = 0
grow_vertical = 0

View File

@@ -9,7 +9,7 @@ func _ready():
Global.mouse = self
func _process(delta):
if Global.main.editMode:
if Global.main and Global.main.editMode:
if text != "":
label.text = text
visible = true

View File

@@ -0,0 +1,122 @@
shader_type canvas_item;
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
uniform int blend_mode : hint_range(0, 11) = 0;
// Blend mode constants:
// 0: Normal
// 1: Multiply
// 2: Screen
// 3: Overlay
// 4: Soft Light
// 5: Hard Light
// 6: Color Dodge
// 7: Color Burn
// 8: Darken
// 9: Lighten
// 10: Add (Linear Dodge)
// 11: Subtract
vec3 multiply_blend(vec3 base, vec3 overlay) {
return base * overlay;
}
vec3 screen_blend(vec3 base, vec3 overlay) {
return 1.0 - (1.0 - base) * (1.0 - overlay);
}
vec3 overlay_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = base.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
result.g = base.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
result.b = base.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
return result;
}
vec3 soft_light_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r + base.r * base.r * (1.0 - 2.0 * overlay.r) :
sqrt(base.r) * (2.0 * overlay.r - 1.0) + 2.0 * base.r * (1.0 - overlay.r);
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g + base.g * base.g * (1.0 - 2.0 * overlay.g) :
sqrt(base.g) * (2.0 * overlay.g - 1.0) + 2.0 * base.g * (1.0 - overlay.g);
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b + base.b * base.b * (1.0 - 2.0 * overlay.b) :
sqrt(base.b) * (2.0 * overlay.b - 1.0) + 2.0 * base.b * (1.0 - overlay.b);
return result;
}
vec3 hard_light_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
return result;
}
vec3 color_dodge_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r >= 1.0 ? 1.0 : min(1.0, base.r / (1.0 - overlay.r));
result.g = overlay.g >= 1.0 ? 1.0 : min(1.0, base.g / (1.0 - overlay.g));
result.b = overlay.b >= 1.0 ? 1.0 : min(1.0, base.b / (1.0 - overlay.b));
return result;
}
vec3 color_burn_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.r) / overlay.r);
result.g = overlay.g <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.g) / overlay.g);
result.b = overlay.b <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.b) / overlay.b);
return result;
}
vec3 darken_blend(vec3 base, vec3 overlay) {
return min(base, overlay);
}
vec3 lighten_blend(vec3 base, vec3 overlay) {
return max(base, overlay);
}
vec3 add_blend(vec3 base, vec3 overlay) {
return min(vec3(1.0), base + overlay);
}
vec3 subtract_blend(vec3 base, vec3 overlay) {
return max(vec3(0.0), base - overlay);
}
void fragment() {
vec4 tex_color = texture(TEXTURE, UV);
// Apply blend mode (only to RGB, preserve alpha channel behavior)
vec3 final_color;
if (blend_mode == 0) { // Normal
final_color = tex_color.rgb;
} else if (blend_mode == 1) { // Multiply
final_color = multiply_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 2) { // Screen
final_color = screen_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 3) { // Overlay
final_color = overlay_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 4) { // Soft Light
final_color = soft_light_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 5) { // Hard Light
final_color = hard_light_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 6) { // Color Dodge
final_color = color_dodge_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 7) { // Color Burn
final_color = color_burn_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 8) { // Darken
final_color = darken_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 9) { // Lighten
final_color = lighten_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 10) { // Add (Linear Dodge)
final_color = add_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 11) { // Subtract
final_color = subtract_blend(vec3(1.0), tex_color.rgb);
} else {
final_color = tex_color.rgb;
}
COLOR = vec4(final_color, tex_color.a * opacity);
}

View File

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

View File

@@ -28,7 +28,7 @@ var imageSize = Vector2.ZERO
@onready var wob = $WobbleOrigin
@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
@onready var opacityShader = preload("res://ui_scenes/selectedSprite/opacity.gdshader")
@onready var opacityBlendShader = preload("res://ui_scenes/selectedSprite/opacity_blend.gdshader")
var opacityMaterial = null
@@ -79,20 +79,25 @@ var ignoreBounce = false
#Animation
var frames = 1
var animSpeed = 0
var animTime = 0.0 # Time accumulator for animations
var remadePolygon = false
var clipped = false
var tick = 0
var time = 0.0 # Time accumulator for physics calculations
#Opacity
var spriteOpacity = 1.0
var affectChildrenOpacity = false
var blendMode = 0 # Default to Normal blend mode
#Vis toggle
var toggle = "null"
#Wobble Sync Group
var wobbleSyncGroup = ""
func _ready():
Global.main.spriteVisToggles.connect(visToggle)
@@ -170,6 +175,8 @@ func _ready():
setClip(clipped)
# Always initialize opacity shader
updateShaderOpacity()
if Global.filtering:
sprite.texture_filter = 2
@@ -224,7 +231,7 @@ func replaceSprite(pathNew):
remakePolygon()
func _process(delta):
tick += 1
time += delta
if Global.heldSprite == self:
grabArea.visible = true
@@ -234,6 +241,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
@@ -251,13 +268,17 @@ func _process(delta):
talkBlink()
animation()
animation(delta)
func animation():
var speed = max(float(animSpeed),Engine.max_fps*6.0)
func animation(delta):
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:
sprite.frame = 0
else:
@@ -269,7 +290,7 @@ func setZIndex():
sprite.z_index = z
func talkBlink():
var faded = 0.2 * int(Global.main.editMode)
var faded = 0.2 * int(Global.main and Global.main.editMode)
var value = (showOnTalk + (showOnBlink*3)) + (int(Global.speaking)*10) + (int(Global.blink)*20)
var yes = [0,10,20,30,1,21,12,32,3,13,4,15,26,36,27,38].has(int(value))
var baseAlpha = max(int(yes), faded)
@@ -277,7 +298,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
@@ -291,7 +317,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):
@@ -348,31 +407,46 @@ func drag(delta):
if dragSpeed == 0:
dragger.global_position = wob.global_position
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
func wobble():
wob.position.x = sin(tick*xFrq)*xAmp
wob.position.y = sin(tick*yFrq)*yAmp
# Use Global.main.physicsTimeMultiplier for consistent physics timing
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)
#Calculate Max angle
yvel = clamp(yvel,rLimitMin,rLimitMax)
yvel = clamp(yvel, rLimitMin, rLimitMax)
# Add rotation wobble as percentage of rotational limits range
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
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 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):
grabArea.monitorable = enable
@@ -440,17 +514,24 @@ 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 blend shader to ensure it's loaded
if opacityMaterial == null:
opacityMaterial = ShaderMaterial.new()
opacityMaterial.shader = opacityBlendShader
opacityMaterial.set_shader_parameter("opacity", finalOpacity)
opacityMaterial.set_shader_parameter("blend_mode", blendMode)
sprite.material = opacityMaterial
## Update the blend mode and refresh the shader
func updateBlendMode(newBlendMode: int):
if BlendModeManager.is_valid_blend_mode(newBlendMode):
blendMode = newBlendMode
updateShaderOpacity() # Refresh shader with new blend mode
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

@@ -5,15 +5,140 @@ extends Node2D
@onready var parentSpin = $SubViewportContainer2/SubViewport/Node3D/Sprite3D
@onready var spriteRotDisplay = $RotationalLimits/RotBack/SpriteDisplay
@onready var spriteRotDisplay = $VBoxContainer/RotationalLimitsSection/RotBack/SpriteDisplay
@onready var coverCollider = $Area2D/CollisionShape2D
@onready var wobbleSyncControl = null # Will be assigned in _ready()
@onready var blendModeDropdown = $VBoxContainer/RenderingSection/blendModeContainer/blendModeDropdown
## Slider management system
var slider_manager: SliderManager = null
func _ready():
Global.spriteEdit = self
# Initialize slider management system
slider_manager = SliderManager.new(self)
_setup_slider_components()
# Find the wobble sync control node
wobbleSyncControl = find_child("WobbleSyncControl")
if wobbleSyncControl == null:
print("Warning: WobbleSyncControl node not found in sprite viewer")
# Set up blend mode dropdown
setupBlendModeDropdown()
## Set up all slider components
func _setup_slider_components():
# Initialize all the new slider components and add them to the manager
_setup_drag_slider()
_setup_wobble_sliders()
_setup_rotation_sliders()
_setup_limit_sliders()
_setup_animation_sliders()
_setup_opacity_slider()
## Set up drag slider
func _setup_drag_slider():
var drag_slider = $VBoxContainer/PhysicsSection/DragSlider
if drag_slider:
slider_manager.add_slider(drag_slider)
drag_slider.value_changed.connect(_on_drag_slider_value_changed)
## Set up wobble sliders
func _setup_wobble_sliders():
var wobble_sliders = [
$VBoxContainer/WobbleSection/xFrqSlider,
$VBoxContainer/WobbleSection/xAmpSlider,
$VBoxContainer/WobbleSection/yFrqSlider,
$VBoxContainer/WobbleSection/yAmpSlider,
$VBoxContainer/WobbleSection/rFrqSlider,
$VBoxContainer/WobbleSection/rAmpSlider
]
for slider in wobble_sliders:
if slider:
slider_manager.add_slider(slider)
slider.value_changed.connect(_on_wobble_slider_changed)
## Set up rotation sliders
func _setup_rotation_sliders():
var rotation_sliders = [
$VBoxContainer/PhysicsSection/rDragSlider,
$VBoxContainer/PhysicsSection/squashSlider
]
for slider in rotation_sliders:
if slider:
slider_manager.add_slider(slider)
slider.value_changed.connect(_on_rotation_slider_changed)
## Set up rotational limit sliders
func _setup_limit_sliders():
var limit_sliders = [
$VBoxContainer/RotationalLimitsSection/rotLimitMinSlider,
$VBoxContainer/RotationalLimitsSection/rotLimitMaxSlider
]
for slider in limit_sliders:
if slider:
slider_manager.add_slider(slider)
slider.value_changed.connect(_on_limit_slider_changed)
## Set up animation sliders
func _setup_animation_sliders():
var animation_sliders = [
$VBoxContainer/AnimationSection/animFramesSlider,
$VBoxContainer/AnimationSection/animSpeedSlider
]
for slider in animation_sliders:
if slider:
slider_manager.add_slider(slider)
slider.value_changed.connect(_on_animation_slider_changed)
## Set up opacity slider
func _setup_opacity_slider():
var opacity_slider = $VBoxContainer/RenderingSection/opacitySlider
if opacity_slider:
slider_manager.add_slider(opacity_slider)
opacity_slider.value_changed.connect(_on_opacity_slider_changed)
## Generic slider change handlers
func _on_drag_slider_value_changed(value: float):
if Global.heldSprite:
Global.heldSprite.dragSpeed = value
func _on_wobble_slider_changed(value: float):
# Wobble sliders handle their own parameter updates
# Just update the wobble sync control if needed
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_rotation_slider_changed(value: float):
# Rotation sliders handle their own parameter updates
if Global.heldSprite:
changeRotLimit() # Update the rotation limit display
func _on_limit_slider_changed(value: float):
# Limit sliders handle their own parameter updates
if Global.heldSprite:
changeRotLimit() # Update the rotation limit display
func _on_animation_slider_changed(value: float):
# Animation sliders handle their own parameter updates
if Global.heldSprite:
# Special handling for frames slider
var frames_slider = $VBoxContainer/AnimationSection/animFramesSlider
if frames_slider and frames_slider.value != Global.heldSprite.frames:
spriteSpin.hframes = Global.heldSprite.frames
func _on_opacity_slider_changed(value: float):
# Opacity slider handles its own parameter updates via sprite_property
pass
func setImage():
if Global.heldSprite == null:
return
@@ -27,67 +152,48 @@ func setImage():
var displaySize = Global.heldSprite.imageData.get_size().y
spriteRotDisplay.scale = Vector2(1,1) * (150.0/displaySize)
$Slider/Label.text = "drag: " + str(Global.heldSprite.dragSpeed)
$Slider/DragSlider.value = Global.heldSprite.dragSpeed
# Update all slider components with the current sprite
if slider_manager:
slider_manager.update_all_sliders(Global.heldSprite)
# Update individual sliders with current values
_update_all_slider_values()
$WobbleControl/xFrqLabel.text = "x frequency: " + str(Global.heldSprite.xFrq)
$WobbleControl/xAmpLabel.text = "x amplitude: " + str(Global.heldSprite.xAmp)
$Position/fileTitle.text = Global.heldSprite.path.replace("user://", "")
$WobbleControl/xFrq.value = Global.heldSprite.xFrq
$WobbleControl/xAmp.value = Global.heldSprite.xAmp
$WobbleControl/yFrqLabel.text = "y frequency: " + str(Global.heldSprite.yFrq)
$WobbleControl/yAmpLabel.text = "y amplitude: " + str(Global.heldSprite.yAmp)
$WobbleControl/yFrq.value = Global.heldSprite.yFrq
$WobbleControl/yAmp.value = Global.heldSprite.yAmp
$VBoxContainer/PhysicsSection/BounceVelocity.button_pressed = Global.heldSprite.ignoreBounce
$VBoxContainer/RenderingSection/ClipLinked.button_pressed = Global.heldSprite.clipped
$WobbleControl/rFrqLabel.text = "rotation frequency: " + str(Global.heldSprite.rFrq)
$WobbleControl/rAmpLabel.text = "rotation percentage: " + str(Global.heldSprite.rAmp) + "%"
$WobbleControl/rFrq.value = Global.heldSprite.rFrq
$WobbleControl/rAmp.value = Global.heldSprite.rAmp
$VBoxContainer/Buttons/SpeakingWrapper/Speaking.frame = Global.heldSprite.showOnTalk
$VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame = Global.heldSprite.showOnBlink
$Rotation/rDragLabel.text = "rotational drag: " + str(Global.heldSprite.rdragStr)
$Rotation/rDrag.value = Global.heldSprite.rdragStr
$VBoxContainer/RenderingSection/affectChildrenCheck.button_pressed = Global.heldSprite.affectChildrenOpacity
$VBoxContainer/RenderingSection/blendModeContainer/blendModeDropdown.selected = Global.heldSprite.blendMode
$Buttons/Speaking.frame = Global.heldSprite.showOnTalk
$Buttons/Blinking.frame = Global.heldSprite.showOnBlink
$RotationalLimits/rotLimitMin.value = Global.heldSprite.rLimitMin
$RotationalLimits/RotLimitMin.text = "rotational limit min: " + str(Global.heldSprite.rLimitMin)
$RotationalLimits/rotLimitMax.value = Global.heldSprite.rLimitMax
$RotationalLimits/RotLimitMax.text = "rotational limit max: " + str(Global.heldSprite.rLimitMax)
$Rotation/squashlabel.text = "squash: " + str(Global.heldSprite.stretchAmount)
$Rotation/squash.value = Global.heldSprite.stretchAmount
$Position/fileTitle.text = Global.heldSprite.path
$Buttons/CheckBox.button_pressed = Global.heldSprite.ignoreBounce
$Buttons/ClipLinked.button_pressed = Global.heldSprite.clipped
$Animation/animSpeedLabel.text = "animation speed: " + str(Global.heldSprite.animSpeed)
$Animation/animSpeed.value = Global.heldSprite.animSpeed
$Animation/animFramesLabel.text = "sprite frames: " + str(Global.heldSprite.frames)
$Animation/animFrames.value = Global.heldSprite.frames
$Opacity/opacityLabel.text = "opacity: " + str(int(Global.heldSprite.spriteOpacity * 100)) + "%"
$Opacity/opacitySlider.value = Global.heldSprite.spriteOpacity
$Opacity/affectChildrenCheck.button_pressed = Global.heldSprite.affectChildrenOpacity
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
changeRotLimit()
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
$VBoxContainer/Buttons/UnlinkWrapper/Unlink.visible = false
parentSpin.visible = false
else:
$Buttons/Unlink.visible = true
$VBoxContainer/Buttons/UnlinkWrapper/Unlink.visible = true
var nodes = get_tree().get_nodes_in_group(str(Global.heldSprite.parentId))
@@ -98,6 +204,92 @@ func setImage():
parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y
parentSpin.hframes = nodes[0].frames
parentSpin.visible = true
## Update all slider components with current sprite values
func _update_all_slider_values():
if not Global.heldSprite:
return
# Update drag slider
var drag_slider = $VBoxContainer/PhysicsSection/DragSlider
if drag_slider:
drag_slider.set_value(Global.heldSprite.dragSpeed)
# Update wobble sliders
var wobble_sliders = {
$VBoxContainer/WobbleSection/xFrqSlider: Global.heldSprite.xFrq,
$VBoxContainer/WobbleSection/xAmpSlider: Global.heldSprite.xAmp,
$VBoxContainer/WobbleSection/yFrqSlider: Global.heldSprite.yFrq,
$VBoxContainer/WobbleSection/yAmpSlider: Global.heldSprite.yAmp,
$VBoxContainer/WobbleSection/rFrqSlider: Global.heldSprite.rFrq,
$VBoxContainer/WobbleSection/rAmpSlider: Global.heldSprite.rAmp
}
for slider in wobble_sliders:
if slider:
slider.set_value(wobble_sliders[slider])
# Update rotation sliders
var rotation_sliders = {
$VBoxContainer/PhysicsSection/rDragSlider: Global.heldSprite.rdragStr,
$VBoxContainer/PhysicsSection/squashSlider: Global.heldSprite.stretchAmount
}
for slider in rotation_sliders:
if slider:
slider.set_value(rotation_sliders[slider])
# Update limit sliders
var limit_sliders = {
$VBoxContainer/RotationalLimitsSection/rotLimitMinSlider: Global.heldSprite.rLimitMin,
$VBoxContainer/RotationalLimitsSection/rotLimitMaxSlider: Global.heldSprite.rLimitMax
}
for slider in limit_sliders:
if slider:
slider.set_value(limit_sliders[slider])
# Update animation sliders
var animation_sliders = {
$VBoxContainer/AnimationSection/animFramesSlider: Global.heldSprite.frames,
$VBoxContainer/AnimationSection/animSpeedSlider: Global.heldSprite.animSpeed
}
for slider in animation_sliders:
if slider:
slider.set_value(animation_sliders[slider])
# Update opacity slider
var opacity_slider = $VBoxContainer/RenderingSection/opacitySlider
if opacity_slider:
opacity_slider.set_value(Global.heldSprite.spriteOpacity)
## Update wobble control states based on sync status
func updateWobbleControlStates():
if Global.heldSprite == null:
return
# Use slider manager if available
if slider_manager:
slider_manager.update_wobble_control_states(Global.heldSprite)
# Also update individual wobble sliders for visual feedback
var is_synced = Global.heldSprite.isSynced()
var tint = Color(1.0, 1.0, 0.9) if is_synced else Color.WHITE
var wobble_sliders = [
$VBoxContainer/WobbleSection/xFrqSlider,
$VBoxContainer/WobbleSection/xAmpSlider,
$VBoxContainer/WobbleSection/yFrqSlider,
$VBoxContainer/WobbleSection/yAmpSlider,
$VBoxContainer/WobbleSection/rFrqSlider,
$VBoxContainer/WobbleSection/rAmpSlider
]
for slider in wobble_sliders:
if slider and slider.has_method("set_modulate_color"):
slider.set_modulate_color(tint)
slider.set_editable(true) # Keep enabled even when synced
func _process(delta):
@@ -111,9 +303,9 @@ func _process(delta):
spriteSpin.rotate_y(delta*4.0)
parentSpin.rotate_y(delta*4.0)
$Position/Label.text = "position X : "+str(obj.position.x)+" Y: " + str(obj.position.y)
$Position/Label2.text = "offset X : "+str(obj.offset.x)+" Y: " + str(obj.offset.y)
$Position/Label3.text = "layer : "+str(obj.z)
$VBoxContainer/InfoSection/PositionLabel.text = "position X : "+str(obj.position.x)+" Y: " + str(obj.position.y)
$VBoxContainer/InfoSection/OffsetLabel.text = "offset X : "+str(obj.offset.x)+" Y: " + str(obj.offset.y)
$VBoxContainer/InfoSection/LayerLabel.text = "layer : "+str(obj.z)
#Sprite Rotational Limit Display
@@ -121,65 +313,30 @@ func _process(delta):
var minimum = Global.heldSprite.rLimitMin
spriteRotDisplay.rotation_degrees = sin(Global.animationTick*0.05)*(size/2.0)+(minimum+(size/2.0))
$RotationalLimits/RotBack/RotLineDisplay3.rotation_degrees = spriteRotDisplay.rotation_degrees
func _on_drag_slider_value_changed(value):
if Global.heldSprite != null:
$Slider/Label.text = "drag: " + str(value)
Global.heldSprite.dragSpeed = value
func _on_x_frq_value_changed(value):
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
Global.heldSprite.xFrq = value
func _on_x_amp_value_changed(value):
$WobbleControl/xAmpLabel.text = "x amplitude: " + str(value)
Global.heldSprite.xAmp = value
func _on_y_frq_value_changed(value):
$WobbleControl/yFrqLabel.text = "y frequency: " + str(value)
Global.heldSprite.yFrq = value
func _on_y_amp_value_changed(value):
$WobbleControl/yAmpLabel.text = "y amplitude: " + str(value)
Global.heldSprite.yAmp = value
func _on_r_frq_value_changed(value):
$WobbleControl/rFrqLabel.text = "rotation frequency: " + str(value)
Global.heldSprite.rFrq = value
func _on_r_amp_value_changed(value):
$WobbleControl/rAmpLabel.text = "rotation amplitude: " + str(value) + "%"
Global.heldSprite.rAmp = value
func _on_r_drag_value_changed(value):
$Rotation/rDragLabel.text = "rotational drag: " + str(value)
Global.heldSprite.rdragStr = value
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay3.rotation_degrees = spriteRotDisplay.rotation_degrees
func _on_speaking_pressed():
var f = $Buttons/Speaking.frame
var f = $VBoxContainer/Buttons/SpeakingWrapper/Speaking.frame
f = (f+1) % 3
$Buttons/Speaking.frame = f
$VBoxContainer/Buttons/SpeakingWrapper/Speakingaking.frame = f
Global.heldSprite.showOnTalk = f
func _on_blinking_pressed():
var f = $Buttons/Blinking.frame
var f = $VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame
f = (f+1) % 3
$Buttons/Blinking.frame = f
$VBoxContainer/Buttons/BlinkingWrapper/Blinking.frame = f
Global.heldSprite.showOnBlink = f
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
@@ -192,38 +349,26 @@ func _on_unlink_pressed():
setImage()
func _on_rot_limit_min_value_changed(value):
$RotationalLimits/RotLimitMin.text = "rotational limit min: " + str(value)
Global.heldSprite.rLimitMin = value
changeRotLimit()
func _on_rot_limit_max_value_changed(value):
$RotationalLimits/RotLimitMax.text = "rotational limit max: " + str(value)
Global.heldSprite.rLimitMax = value
changeRotLimit()
func changeRotLimit():
$RotationalLimits/RotBack/rotLimitBar.value = Global.heldSprite.rLimitMax - Global.heldSprite.rLimitMin
$RotationalLimits/RotBack/rotLimitBar.rotation_degrees = Global.heldSprite.rLimitMin + 90
$VBoxContainer/RotationalLimitsSection/RotBack/rotLimitBar.value = Global.heldSprite.rLimitMax - Global.heldSprite.rLimitMin
$VBoxContainer/RotationalLimitsSection/RotBack/rotLimitBar.rotation_degrees = Global.heldSprite.rLimitMin + 90
$RotationalLimits/RotBack/RotLineDisplay.rotation_degrees = Global.heldSprite.rLimitMin
$RotationalLimits/RotBack/RotLineDisplay2.rotation_degrees = Global.heldSprite.rLimitMax
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay.rotation_degrees = Global.heldSprite.rLimitMin
$VBoxContainer/RotationalLimitsSection/RotBack/RotLineDisplay2.rotation_degrees = Global.heldSprite.rLimitMax
func setLayerButtons():
var a = Global.heldSprite.costumeLayers.duplicate()
$Layers/Layer1.frame = 1-a[0]
$Layers/Layer2.frame = 1-a[1]
$Layers/Layer3.frame = 1-a[2]
$Layers/Layer4.frame = 1-a[3]
$Layers/Layer5.frame = 1-a[4]
$Layers/Layer6.frame = 1-a[5]
$Layers/Layer7.frame = 1-a[6]
$Layers/Layer8.frame = 1-a[7]
$Layers/Layer9.frame = 1-a[8]
$Layers/Layer10.frame = 1-a[9]
$VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1.frame = 1-a[0]
$VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2.frame = 1-a[1]
$VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3.frame = 1-a[2]
$VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4.frame = 1-a[3]
$VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5.frame = 1-a[4]
$VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6.frame = 1-a[5]
$VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7.frame = 1-a[6]
$VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8.frame = 1-a[7]
$VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9.frame = 1-a[8]
$VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10.frame = 1-a[9]
var nodes = get_tree().get_nodes_in_group("saved")
for sprite in nodes:
@@ -314,71 +459,62 @@ func layerSelected():
var newPos = Vector2.ZERO
match Global.main.costume:
1:
newPos = $Layers/Layer1.position
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer1Container/Layer1.position
2:
newPos = $Layers/Layer2.position
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer2Container/Layer2.position
3:
newPos = $Layers/Layer3.position
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer3Container/Layer3.position
4:
newPos = $Layers/Layer4.position
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer4Container/Layer4.position
5:
newPos = $Layers/Layer5.position
newPos = $VBoxContainer/LayersSection/HBoxContainer/Layer5Container/Layer5.position
6:
newPos = $Layers/Layer6.position
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer6Container/Layer6.position
7:
newPos = $Layers/Layer7.position
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer7Container/Layer7.position
8:
newPos = $Layers/Layer8.position
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer8Container/Layer8.position
9:
newPos = $Layers/Layer9.position
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer9Container/Layer9.position
10:
newPos = $Layers/Layer10.position
$Layers/Select.position = newPos
func _on_squash_value_changed(value):
$Rotation/squashlabel.text = "squash: " + str(value)
Global.heldSprite.stretchAmount = value
func _on_check_box_toggled(button_pressed):
Global.heldSprite.ignoreBounce = button_pressed
func _on_anim_speed_value_changed(value):
$Animation/animSpeedLabel.text = "animation speed: " + str(value)
Global.heldSprite.animSpeed = value
func _on_anim_frames_value_changed(value):
$Animation/animFramesLabel.text = "sprite frames: " + str(value)
Global.heldSprite.frames = value
spriteSpin.hframes = Global.heldSprite.frames
Global.heldSprite.changeFrames()
newPos = $VBoxContainer/LayersSection/HBoxContainer2/Layer10Container/Layer10.position
func _on_clip_linked_toggled(button_pressed):
Global.heldSprite.setClip(button_pressed)
func _on_check_box_toggled(button_pressed):
Global.heldSprite.ignoreBounce = button_pressed
func _on_delete_pressed():
Global.heldSprite.toggle = "null"
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
Global.heldSprite.makeVis()
func _on_set_toggle_pressed():
$VisToggle/setToggle/Label.text = "toggle: AWAITING INPUT"
$VBoxContainer/BoxContainer/setToggle/Labelbel.text = "toggle: AWAITING INPUT"
await Global.main.fatfuckingballs
var keys = await Global.main.spriteVisToggles
var key = keys[0]
Global.heldSprite.toggle = key
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
func _on_opacity_slider_value_changed(value):
$Opacity/opacityLabel.text = "opacity: " + str(int(value * 100)) + "%"
Global.heldSprite.spriteOpacity = value
Global.heldSprite.updateOpacity()
$VBoxContainer/BoxContainer/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
func _on_affect_children_check_toggled(button_pressed):
Global.heldSprite.affectChildrenOpacity = button_pressed
Global.heldSprite.updateOpacity()
## Set up the blend mode dropdown with all available blend modes
func setupBlendModeDropdown():
blendModeDropdown.clear()
var blendModeNames = BlendModeManager.get_blend_mode_names()
for mode_name in blendModeNames:
blendModeDropdown.add_item(mode_name)
## Handle blend mode dropdown selection
func _on_blend_mode_dropdown_item_selected(index: int):
if Global.heldSprite != null:
Global.heldSprite.updateBlendMode(index)
Global.pushUpdate("Blend mode set to: " + BlendModeManager.get_blend_mode_name(index))

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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