Compare commits

...

39 Commits

Author SHA1 Message Date
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
e8191e2de8 Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 05:34:26 +10:00
b7b7d7cdec test some build description stuff out !build 2025-07-20 05:34:24 +10:00
fc6e4a1028 Update README.md 2025-07-20 05:28:11 +10:00
1dfe21363b update wording 2025-07-20 04:42:36 +10:00
fde3f23ed5 build trigger 2025-07-20 04:17:21 +10:00
79e54c28da Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 04:15:13 +10:00
a8f327ae5a Add opacity slider with shader-based rendering that preserves clip mask behavior
- Added opacity slider (0-100%) in sprite options with 'affect children' checkbox
- Implemented hierarchical opacity that affects entire sprite hierarchy when enabled
- Created shader-based opacity system that doesn't interfere with clip mask functionality
- Clip masks now work with original image pixels regardless of opacity settings
- Removed shadow effects from selected sprites to prevent visual conflicts
- Updated save/load system to persist opacity properties with backward compatibility

Fixes clipping behavior issues where opacity changes previously affected clip mask detection
2025-07-20 04:14:58 +10:00
1295bcbcba Add opacity slider with shader-based rendering that preserves clip mask behavior
- Added opacity slider (0-100%) in sprite options with 'affect children' checkbox
- Implemented hierarchical opacity that affects entire sprite hierarchy when enabled
- Created shader-based opacity system that doesn't interfere with clip mask functionality
- Clip masks now work with original image pixels regardless of opacity settings
- Removed shadow effects from selected sprites to prevent visual conflicts
- Updated save/load system to persist opacity properties with backward compatibility

Fixes clipping behavior issues where opacity changes previously affected clip mask detection
2025-07-20 04:07:41 +10:00
758f2ca885 fix key listening again 2025-07-20 02:48:16 +10:00
524920d140 Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 02:38:29 +10:00
f9c72448d9 Merge pull request #2 from litruv/copilot/fix-1
Fix costume key assignment regression - resolves "awaiting input" hang
2025-07-20 02:31:13 +10:00
copilot-swe-agent[bot]
135e63ef52 Fix costume key assignment regression by handling mouse click cancellation
Co-authored-by: litruv <3798007+litruv@users.noreply.github.com>
2025-07-19 16:25:12 +00:00
copilot-swe-agent[bot]
c86e68d383 Initial plan 2025-07-19 16:17:53 +00:00
f226c14c00 Revert "adding mask options"
This reverts commit c74dccf1cf.
2025-07-20 02:12:22 +10:00
d345e700fc Merge branch 'master' of https://github.com/litruv/PNGTuber-Plus 2025-07-20 00:03:50 +10:00
bc187cff09 Improve GitHub Actions workflow versioning and release formatting
- Add automated release info generation with date-based versioning
- Generate semantic version tags (v2025.MM.DD-buildNUMBER format)
- Implement dynamic build numbering based on commit count
- Streamline release name format to 'PNGTuber+ vDATE Build NUMBER'
- Update release body with proper credits and installation instructions
- Remove hardcoded release descriptions in favor of standardized format
2025-07-20 00:03:23 +10:00
fc4cacdc9f Update release name format in build workflow 2025-07-19 23:58:28 +10:00
848361d19b update versioning to be not awful 2025-07-19 23:52:46 +10:00
711865c6a4 update kaiakairos social to bluesky 2025-07-19 23:42:03 +10:00
d231b730f7 Add experimental mic loudness UI toggle to settings 2025-07-19 22:16:40 +10:00
8ba361f3c2 Integrate microphone improvements from k0ffinz repository
- Add experimental microphone loudness detection feature
- Remove microphone restart interval for better stability
- Improve microphone initialization and error handling
- Fix audio delay issues by setting channel_disable_time=0.0
- Update settings UI to include experimental mic loudness toggle
- Add credits to k0ffinz/Ralsi for microphone improvements

These changes improve audio responsiveness and fix various
microphone-related issues including WASAPI errors and growing
audio delay problems.
2025-07-19 22:00:53 +10:00
c74dccf1cf adding mask options 2025-07-19 21:52:49 +10:00
5c51691ba7 Add Windows build automation with GitHub Actions
- Add comprehensive GitHub Actions workflow for Windows builds
- Create local and CI-specific batch files for building
- Configure export templates and build optimization
- Update export presets for proper .pck embedding and git plugin exclusion
- Set up automated releases with proper build artifacts
2025-07-19 20:36:17 +10:00
39 changed files with 2007 additions and 207 deletions

View File

@@ -1,5 +1,6 @@
name: Build Windows
on:
push:
branches: [ master, main ]
@@ -14,24 +15,77 @@ env:
jobs:
export-windows:
name: Windows Export
runs-on: ubuntu-20.04
container:
image: barichello/godot-ci:4.4.1
runs-on: self-hosted
if: contains(github.event.head_commit.message, '!build')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup
- name: Debug Environment
run: |
mkdir -v -p ~/.local/share/godot/export_templates/
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable
Write-Host "Current directory: $(Get-Location)"
Write-Host "Godot executable exists: $(Test-Path 'D:\Godot\Godot_v4.4.1-stable_win64.exe')"
Write-Host "Export name: $env:EXPORT_NAME"
Write-Host "Project file exists: $(Test-Path 'project.godot')"
Get-ChildItem -Path . -Filter "*.cfg" | Select-Object Name
- name: Verify Export Templates
run: |
$templateDir = "C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Roaming\Godot\export_templates\4.4.1.stable"
Write-Host "Checking for export templates at: $templateDir"
if (Test-Path $templateDir) {
Write-Host "Export templates found. Contents:"
Get-ChildItem -Path $templateDir | Where-Object {$_.Name -like "*windows*"} | Select-Object Name, Length | Format-Table
# Verify the specific Windows templates we need
$debugTemplate = Join-Path $templateDir "windows_debug_x86_64.exe"
$releaseTemplate = Join-Path $templateDir "windows_release_x86_64.exe"
if ((Test-Path $debugTemplate) -and (Test-Path $releaseTemplate)) {
Write-Host "✅ Required Windows templates found"
} else {
Write-Host "❌ Required Windows templates missing"
exit 1
}
} else {
Write-Host "❌ Export template directory not found!"
exit 1
}
# Check if rcedit is available (it's already in the project)
Write-Host "Checking for rcedit..."
if (Test-Path "rcedit-x64.exe") {
Write-Host "✅ rcedit found in project directory"
} else {
Write-Host "rcedit not found - downloading..."
Invoke-WebRequest -Uri "https://github.com/electron/rcedit/releases/latest/download/rcedit-x64.exe" -OutFile "rcedit-x64.exe"
Write-Host "✅ rcedit downloaded"
}
- name: Windows Build
run: .\build-windows-ci.bat
shell: cmd
- name: Check Build Output
run: |
mkdir -v -p build/windows
godot --headless --verbose --export-release "Windows Desktop" build/windows/$EXPORT_NAME.exe
Write-Host "Build directory contents:"
if (Test-Path "build\windows") {
Get-ChildItem -Path "build\windows" -Recurse | Select-Object Name, Length
} else {
Write-Host "Build directory does not exist!"
}
if (Test-Path "build\windows\PNGTuber-Plus.exe") {
Write-Host "Build SUCCESS: PNGTuber-Plus.exe created"
$fileSize = (Get-Item "build\windows\PNGTuber-Plus.exe").Length
Write-Host "File size: $fileSize bytes"
} else {
Write-Host "Build FAILED: PNGTuber-Plus.exe not found"
exit 1
}
- name: Upload Windows Build
uses: actions/upload-artifact@v4
@@ -43,9 +97,77 @@ jobs:
release:
name: Create Release
needs: export-windows
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
runs-on: self-hosted
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && success()
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate Release Info
id: release_info
run: |
# Get current date and time
$date = Get-Date -Format "yyyy.MM.dd"
$time = Get-Date -Format "HHmm"
# Get short commit hash (first 7 characters)
$shortSha = "${{ github.sha }}".Substring(0, 7)
# Get commit count since last tag (for build number)
$buildNumber = & git rev-list --count HEAD
# Create version-like tag and release name
$tag = "v$date-build$buildNumber"
$releaseName = "PNGTuber+ v$date Build $buildNumber"
Write-Host "Generated tag: $tag"
Write-Host "Generated release name: $releaseName"
# Get the latest release tag to compare against
$latestTag = ""
try {
$latestTag = & git describe --tags --abbrev=0 2>$null
Write-Host "Latest tag found: $latestTag"
} catch {
Write-Host "No previous tags found, showing all commits"
}
# Generate commit history since last tag (or all commits if no tag)
$commitRange = if ($latestTag) { "$latestTag..HEAD" } else { "HEAD" }
# 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`n"
$commits | ForEach-Object {
$parts = $_ -split '\|', 3
$hash = $parts[0]
$subject = $parts[1] -replace '"', '""'
$author = $parts[2]
$commitUrl = "https://github.com/${{ github.repository }}/commit/$hash"
$commitHistory += "- [$hash]($commitUrl) $subject`n"
}
} else {
$commitHistory += "`n`n## Changes Since Last Release`n`nNo new commits since last release.`n"
}
# 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
# 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:
@@ -58,21 +180,18 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: build-${{ github.sha }}
release_name: PNGTuber Plus Build ${{ github.sha }}
tag_name: ${{ steps.release_info.outputs.tag }}
release_name: ${{ steps.release_info.outputs.release_name }}
body: |
Automated build of PNGTuber Plus
## Recent Updates by Litruv
- **StreamDeck Integration**: Add buttons to enable the streamdeck plugin that was built in
- **Rotation Wobble**: New sprite rotation effects with frequency and amplitude controls
- **Enhanced Modifier Keys**: Support for complex key combinations (Ctrl+Shift+Alt+Cmd)
- **Godot 4.4.1 Support**: Updated for latest Godot stable release
${{ steps.release_info.outputs.commit_history }}
## Installation
1. Download PNGTuber-Plus.exe from the assets below
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)
draft: false
prerelease: false

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.export/
.itchio/
.steam/
build/

11
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build PNGTuber-Plus",
"type": "shell",
"command": "cd c:\\DEV\\Sync\\PNGTuber-Plus; .\\build-windows.bat",
"group": "build"
}
]
}

View File

@@ -2,15 +2,85 @@
![Build Status](https://github.com/litruv/PNGTuber-Plus/workflows/Build%20Windows/badge.svg)
### Recent Updates by Litruv
- **GitHub Actions**: Automated Windows builds on every commit
- **StreamDeck Integration**: Add buttons to enable the streamdeck plugin that was built in
- **Rotation Wobble**: New sprite rotation effects with frequency and amplitude controls
- **Enhanced Modifier Keys**: Support for complex key combinations (Ctrl+Shift+Alt+Cmd)
- **Godot 4.4.1 Support**: Updated for latest Godot stable release
A fork of [kaiakairos/PNGTuber-Plus](https://github.com/kaiakairos/PNGTuber-Plus) with some quality-of-life upgrades, editor polish, and a few power-user toys.
## 🙏 Credits
---
- **Original Project**: PNGTuber Plus base application
- **StreamDeck Plugin**: Based on [BoyneGames StreamDeck Godot Plugin](https://github.com/BoyneGames/streamdeck-godot-plugin)
- **Enhanced Features**: Max Litruv Boonzaayer
## 🔧 Fork Changes (Litruv Edition)
### 🎛️ Opacity Controls
Added a proper opacity slider for sprites, with an "affect children" toggle. Clip masks still behave like they should.
![Opacity Demo](https://github.com/user-attachments/assets/d5a05508-1cb1-46e0-8c90-d0aac73398db)
### 🌀 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.
![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:
* Reduced mic startup delay
* Better loudness detection
* General stability improvements
### 🧱 Engine Update
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
* Original project by [kaiakairos](https://github.com/kaiakairos/PNGTuber-Plus)
* StreamDeck plugin from [BoyneGames](https://github.com/BoyneGames/streamdeck-godot-plugin)
* Mic improvements by [k0ffinz](https://github.com/k0ffinz/PNGTuber-Plus)

View File

@@ -29,13 +29,13 @@ var blink = false
var blinkTick = 0
#Audio Listener
var currentMicrophone = null
var playa:AudioStreamPlayer
var speaking = false
var spectrum
var volume = 0
var volumeSensitivity = 0.0
var experimentalMicLoudness = false
var volumeLimit = 0.0
var senseLimit = 0.0
@@ -44,8 +44,6 @@ var senseLimit = 0.0
signal startSpeaking
signal stopSpeaking
var micResetTime = 180
var updatePusherNode = null
var rand = RandomNumberGenerator.new()
@@ -56,28 +54,23 @@ func _ready():
if !Saving.settings.has("useStreamDeck"):
Saving.settings["useStreamDeck"] = false
if Saving.settings.has("secondsToMicReset"):
Global.micResetTime = Saving.settings["secondsToMicReset"]
if Saving.settings.has("experimentalMicLoudness"):
Global.experimentalMicLoudness = Saving.settings["experimentalMicLoudness"]
else:
Saving.settings["secondsToMicReset"] = 180
Saving.settings["experimentalMicLoudness"] = false
createMicrophone()
func createMicrophone():
var playa = AudioStreamPlayer.new()
var mic = AudioStreamMicrophone.new()
playa.stream = mic
playa.autoplay = true
if playa != null:
remove_child(playa)
playa.free()
playa = AudioStreamPlayer.new()
playa.bus = "MIC"
playa.stream = AudioStreamMicrophone.new()
add_child(playa)
currentMicrophone = playa
await get_tree().create_timer(micResetTime).timeout
if currentMicrophone != playa:
return
deleteAllMics()
currentMicrophone = null
await get_tree().create_timer(0.25).timeout
createMicrophone()
await get_tree().create_timer(0.25).timeout # apparently it works to fix WASAPI "Initialize failed with error 0xffffffff88890002" and "init_input_device" errors
playa.play()
func deleteAllMics():
for child in get_children():
@@ -87,8 +80,9 @@ func deleteAllMics():
func _process(delta):
animationTick += 1
volume = spectrum.get_magnitude_for_frequency_range(20, 20000).length()
if currentMicrophone != null:
volume = (AudioServer.get_bus_peak_volume_left_db(1, 0) + AudioServer.get_bus_peak_volume_right_db(1, 0)) / 1600.0 + 0.25 if experimentalMicLoudness else spectrum.get_magnitude_for_frequency_range(20, 20000).length()
if playa != null:
volumeSensitivity = lerp(volumeSensitivity,0.0,delta*2)
if volume>volumeLimit:
@@ -112,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)
@@ -121,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:
@@ -132,7 +126,7 @@ func _process(delta):
scrollSprites()
if !main.fileSystemOpen:
if !main or main.fileSystemOpen:
if Input.is_action_just_pressed("refresh"):
refresh()
@@ -228,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

@@ -54,13 +54,13 @@ var settings = {
"bounce":250,
"gravity":1000,
"maxFPS":60,
"secondsToMicReset":180,
"backgroundColor":var_to_str(Color(0.0,0.0,0.0,0.0)),
"filtering":false,
"costumeKeys":["1","2","3","4","5","6","7","8","9","0"],
"blinkSpeed":1.0,
"blinkChance":200,
"bounceOnCostumeChange":false,
"experimentalMicLoudness":false,
}
var settingsPath = "user://settings.pngtp"

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

40
build-windows-ci.bat Normal file
View File

@@ -0,0 +1,40 @@
@echo off
setlocal
echo Building PNGTuber Plus for Windows (CI Environment)...
rem Create build directory
if not exist "build\windows" mkdir "build\windows"
rem Templates should already be installed by the GitHub Actions workflow
set TEMPLATE_DIR=C:\WINDOWS\ServiceProfiles\NetworkService\AppData\Roaming\Godot\export_templates\4.4.1.stable
echo Checking for export templates at: %TEMPLATE_DIR%
if exist "%TEMPLATE_DIR%" (
echo Export templates found.
dir "%TEMPLATE_DIR%"
) else (
echo ERROR: Export templates not found at %TEMPLATE_DIR%
echo This should have been handled by the GitHub Actions setup step.
exit /b 1
)
rem Build the project
echo Running Godot export...
"D:\Godot\Godot_v4.4.1-stable_win64.exe" --headless --export-release "Windows Desktop" "build\windows\PNGTuber-Plus.exe"
if %ERRORLEVEL% EQU 0 (
echo.
echo Build completed successfully!
echo Output: build\windows\PNGTuber-Plus.exe
if exist "build\windows\PNGTuber-Plus.exe" (
echo File size:
dir "build\windows\PNGTuber-Plus.exe"
)
echo.
) else (
echo.
echo Build failed with error code %ERRORLEVEL%
echo.
exit /b %ERRORLEVEL%
)

View File

@@ -1,22 +1,50 @@
@echo off
setlocal
echo Building PNGTuber Plus for Windows...
:: Create build directory
rem Create build directory
if not exist "build\windows" mkdir "build\windows"
:: Build the project
rem Install export templates if they don't exist
echo Checking for export templates...
set TEMPLATE_DIR=%APPDATA%\Godot\export_templates\4.4.1.stable
if not exist "%TEMPLATE_DIR%" (
echo Export templates not found. Installing...
"D:\Godot\Godot_v4.4.1-stable_win64.exe" --headless --export-pack
timeout /t 5 /nobreak >nul
rem Download templates if the export-pack didn't work
if not exist "%TEMPLATE_DIR%" (
echo Downloading export templates...
powershell -Command "& {Invoke-WebRequest -Uri 'https://github.com/godotengine/godot/releases/download/4.4.1-stable/Godot_v4.4.1-stable_export_templates.tpz' -OutFile 'templates.tpz'}"
rem Rename and extract templates
powershell -Command "& {Rename-Item 'templates.tpz' 'templates.zip'}"
powershell -Command "& {Expand-Archive -Path 'templates.zip' -DestinationPath 'temp_templates' -Force}"
if not exist "%APPDATA%\Godot\export_templates" mkdir "%APPDATA%\Godot\export_templates"
move "temp_templates\templates" "%TEMPLATE_DIR%"
rmdir /s /q "temp_templates"
del "templates.zip"
echo Export templates installed.
)
) else (
echo Export templates found.
)
rem Build the project
echo Running Godot export...
godot --headless --verbose --export-release "Windows Desktop" "build\windows\PNGTuber-Plus.exe"
"D:\Godot\Godot_v4.4.1-stable_win64.exe" --headless --export-release "Windows Desktop" "build\windows\PNGTuber-Plus.exe"
if %ERRORLEVEL% EQU 0 (
echo.
echo Build completed successfully!
echo Build completed successfully!
echo Output: build\windows\PNGTuber-Plus.exe
echo.
pause
) else (
echo.
echo Build failed with error code %ERRORLEVEL%
echo Build failed with error code %ERRORLEVEL%
echo.
pause
)
pause

View File

@@ -3,27 +3,29 @@
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
advanced_options=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=".export/release 1.4.5/steam/PNGTUBER PLUS 1.4.5 STEAM.exe"
exclude_filter="addons/godot-git-plugin/*"
export_path="build/windows/PNGTuber-Plus.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/bptc=true
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
debug/export_console_wrapper=2
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
@@ -42,6 +44,9 @@ application/product_name="PNGTuber Plus"
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
@@ -59,22 +64,30 @@ Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorActi
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
texture_format/bptc=true
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
[preset.1]
name="Linux/X11"
platform="Linux/X11"
platform="Linux"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=".export/release 1.4.5/linux/PNGTUBER PLUS 1.4.5 LINUX.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.1.options]
@@ -82,10 +95,8 @@ custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/bptc=true
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@@ -99,22 +110,30 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
texture_format/bptc=true
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
[preset.2]
name="macOS"
platform="macOS"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=".export/release 1.4.5/mac/PNGTUBER PLUS 1.4.5 MAC.zip"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.2.options]
@@ -132,8 +151,11 @@ application/short_version="1.4.5"
application/version="1.4.5"
application/copyright=""
application/copyright_localized={}
application/min_macos_version="10.12"
application/min_macos_version_x86_64="10.12"
application/min_macos_version_arm64="11.00"
application/export_angle=0
display/high_res=true
application/additional_plist_content=""
xcode/platform_build="14C18"
xcode/sdk_version="13.1"
xcode/sdk_build="22C55"
@@ -166,7 +188,9 @@ codesign/entitlements/app_sandbox/files_downloads=0
codesign/entitlements/app_sandbox/files_pictures=0
codesign/entitlements/app_sandbox/files_music=0
codesign/entitlements/app_sandbox/files_movies=0
codesign/entitlements/app_sandbox/files_user_selected=0
codesign/entitlements/app_sandbox/helper_executables=[]
codesign/entitlements/additional=""
codesign/custom_options=PackedStringArray()
notarization/notarization=0
privacy/microphone_usage_description="So the guy can go up and down!"
@@ -191,6 +215,148 @@ privacy/network_volumes_usage_description=""
privacy/network_volumes_usage_description_localized={}
privacy/removable_volumes_usage_description=""
privacy/removable_volumes_usage_description_localized={}
privacy/tracking_enabled=false
privacy/tracking_domains=PackedStringArray()
privacy/collected_data/name/collected=false
privacy/collected_data/name/linked_to_user=false
privacy/collected_data/name/used_for_tracking=false
privacy/collected_data/name/collection_purposes=0
privacy/collected_data/email_address/collected=false
privacy/collected_data/email_address/linked_to_user=false
privacy/collected_data/email_address/used_for_tracking=false
privacy/collected_data/email_address/collection_purposes=0
privacy/collected_data/phone_number/collected=false
privacy/collected_data/phone_number/linked_to_user=false
privacy/collected_data/phone_number/used_for_tracking=false
privacy/collected_data/phone_number/collection_purposes=0
privacy/collected_data/physical_address/collected=false
privacy/collected_data/physical_address/linked_to_user=false
privacy/collected_data/physical_address/used_for_tracking=false
privacy/collected_data/physical_address/collection_purposes=0
privacy/collected_data/other_contact_info/collected=false
privacy/collected_data/other_contact_info/linked_to_user=false
privacy/collected_data/other_contact_info/used_for_tracking=false
privacy/collected_data/other_contact_info/collection_purposes=0
privacy/collected_data/health/collected=false
privacy/collected_data/health/linked_to_user=false
privacy/collected_data/health/used_for_tracking=false
privacy/collected_data/health/collection_purposes=0
privacy/collected_data/fitness/collected=false
privacy/collected_data/fitness/linked_to_user=false
privacy/collected_data/fitness/used_for_tracking=false
privacy/collected_data/fitness/collection_purposes=0
privacy/collected_data/payment_info/collected=false
privacy/collected_data/payment_info/linked_to_user=false
privacy/collected_data/payment_info/used_for_tracking=false
privacy/collected_data/payment_info/collection_purposes=0
privacy/collected_data/credit_info/collected=false
privacy/collected_data/credit_info/linked_to_user=false
privacy/collected_data/credit_info/used_for_tracking=false
privacy/collected_data/credit_info/collection_purposes=0
privacy/collected_data/other_financial_info/collected=false
privacy/collected_data/other_financial_info/linked_to_user=false
privacy/collected_data/other_financial_info/used_for_tracking=false
privacy/collected_data/other_financial_info/collection_purposes=0
privacy/collected_data/precise_location/collected=false
privacy/collected_data/precise_location/linked_to_user=false
privacy/collected_data/precise_location/used_for_tracking=false
privacy/collected_data/precise_location/collection_purposes=0
privacy/collected_data/coarse_location/collected=false
privacy/collected_data/coarse_location/linked_to_user=false
privacy/collected_data/coarse_location/used_for_tracking=false
privacy/collected_data/coarse_location/collection_purposes=0
privacy/collected_data/sensitive_info/collected=false
privacy/collected_data/sensitive_info/linked_to_user=false
privacy/collected_data/sensitive_info/used_for_tracking=false
privacy/collected_data/sensitive_info/collection_purposes=0
privacy/collected_data/contacts/collected=false
privacy/collected_data/contacts/linked_to_user=false
privacy/collected_data/contacts/used_for_tracking=false
privacy/collected_data/contacts/collection_purposes=0
privacy/collected_data/emails_or_text_messages/collected=false
privacy/collected_data/emails_or_text_messages/linked_to_user=false
privacy/collected_data/emails_or_text_messages/used_for_tracking=false
privacy/collected_data/emails_or_text_messages/collection_purposes=0
privacy/collected_data/photos_or_videos/collected=false
privacy/collected_data/photos_or_videos/linked_to_user=false
privacy/collected_data/photos_or_videos/used_for_tracking=false
privacy/collected_data/photos_or_videos/collection_purposes=0
privacy/collected_data/audio_data/collected=false
privacy/collected_data/audio_data/linked_to_user=false
privacy/collected_data/audio_data/used_for_tracking=false
privacy/collected_data/audio_data/collection_purposes=0
privacy/collected_data/gameplay_content/collected=false
privacy/collected_data/gameplay_content/linked_to_user=false
privacy/collected_data/gameplay_content/used_for_tracking=false
privacy/collected_data/gameplay_content/collection_purposes=0
privacy/collected_data/customer_support/collected=false
privacy/collected_data/customer_support/linked_to_user=false
privacy/collected_data/customer_support/used_for_tracking=false
privacy/collected_data/customer_support/collection_purposes=0
privacy/collected_data/other_user_content/collected=false
privacy/collected_data/other_user_content/linked_to_user=false
privacy/collected_data/other_user_content/used_for_tracking=false
privacy/collected_data/other_user_content/collection_purposes=0
privacy/collected_data/browsing_history/collected=false
privacy/collected_data/browsing_history/linked_to_user=false
privacy/collected_data/browsing_history/used_for_tracking=false
privacy/collected_data/browsing_history/collection_purposes=0
privacy/collected_data/search_hhistory/collected=false
privacy/collected_data/search_hhistory/linked_to_user=false
privacy/collected_data/search_hhistory/used_for_tracking=false
privacy/collected_data/search_hhistory/collection_purposes=0
privacy/collected_data/user_id/collected=false
privacy/collected_data/user_id/linked_to_user=false
privacy/collected_data/user_id/used_for_tracking=false
privacy/collected_data/user_id/collection_purposes=0
privacy/collected_data/device_id/collected=false
privacy/collected_data/device_id/linked_to_user=false
privacy/collected_data/device_id/used_for_tracking=false
privacy/collected_data/device_id/collection_purposes=0
privacy/collected_data/purchase_history/collected=false
privacy/collected_data/purchase_history/linked_to_user=false
privacy/collected_data/purchase_history/used_for_tracking=false
privacy/collected_data/purchase_history/collection_purposes=0
privacy/collected_data/product_interaction/collected=false
privacy/collected_data/product_interaction/linked_to_user=false
privacy/collected_data/product_interaction/used_for_tracking=false
privacy/collected_data/product_interaction/collection_purposes=0
privacy/collected_data/advertising_data/collected=false
privacy/collected_data/advertising_data/linked_to_user=false
privacy/collected_data/advertising_data/used_for_tracking=false
privacy/collected_data/advertising_data/collection_purposes=0
privacy/collected_data/other_usage_data/collected=false
privacy/collected_data/other_usage_data/linked_to_user=false
privacy/collected_data/other_usage_data/used_for_tracking=false
privacy/collected_data/other_usage_data/collection_purposes=0
privacy/collected_data/crash_data/collected=false
privacy/collected_data/crash_data/linked_to_user=false
privacy/collected_data/crash_data/used_for_tracking=false
privacy/collected_data/crash_data/collection_purposes=0
privacy/collected_data/performance_data/collected=false
privacy/collected_data/performance_data/linked_to_user=false
privacy/collected_data/performance_data/used_for_tracking=false
privacy/collected_data/performance_data/collection_purposes=0
privacy/collected_data/other_diagnostic_data/collected=false
privacy/collected_data/other_diagnostic_data/linked_to_user=false
privacy/collected_data/other_diagnostic_data/used_for_tracking=false
privacy/collected_data/other_diagnostic_data/collection_purposes=0
privacy/collected_data/environment_scanning/collected=false
privacy/collected_data/environment_scanning/linked_to_user=false
privacy/collected_data/environment_scanning/used_for_tracking=false
privacy/collected_data/environment_scanning/collection_purposes=0
privacy/collected_data/hands/collected=false
privacy/collected_data/hands/linked_to_user=false
privacy/collected_data/hands/used_for_tracking=false
privacy/collected_data/hands/collection_purposes=0
privacy/collected_data/head/collected=false
privacy/collected_data/head/linked_to_user=false
privacy/collected_data/head/used_for_tracking=false
privacy/collected_data/head/collection_purposes=0
privacy/collected_data/other_data_types/collected=false
privacy/collected_data/other_data_types/linked_to_user=false
privacy/collected_data/other_data_types/used_for_tracking=false
privacy/collected_data/other_data_types/collection_purposes=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
@@ -202,3 +368,71 @@ open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
application/min_macos_version="10.12"
[preset.3]
name="Windows Desktop 2"
platform="Windows Desktop"
runnable=false
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=""
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.3.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"

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,40 +159,96 @@ 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()
func followShadow():
shadow.visible = is_instance_valid(Global.heldSprite)
if !shadow.visible:
return
## 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
shadow.global_position = Global.heldSprite.sprite.global_position + Vector2(6,6)
shadow.global_rotation = Global.heldSprite.sprite.global_rotation
shadow.offset = Global.heldSprite.sprite.offset
# Handle mouse movement during edit mode panning
elif editMode and event is InputEventMouseMotion and isPanning:
var totalDelta = event.global_position - initialPanPosition
shadow.texture = Global.heldSprite.sprite.texture
shadow.hframes = Global.heldSprite.sprite.hframes
shadow.frame = Global.heldSprite.sprite.frame
# 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
func isFileSystemOpen():
@@ -173,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
@@ -198,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"):
@@ -221,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
@@ -233,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():
@@ -252,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
@@ -299,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"]
@@ -362,6 +523,14 @@ func _on_load_dialog_file_selected(path):
sprite.clipped = data[item]["clipped"]
if data[item].has("toggle"):
sprite.toggle = data[item]["toggle"]
if data[item].has("spriteOpacity"):
sprite.spriteOpacity = data[item]["spriteOpacity"]
if data[item].has("affectChildrenOpacity"):
sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"]
# 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"])
@@ -370,6 +539,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()
@@ -424,8 +636,18 @@ func _on_save_dialog_file_selected(path):
data[id]["toggle"] = child.toggle
data[id]["spriteOpacity"] = child.spriteOpacity
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
# Save wobble sync group
data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup
id += 1
# Save wobble sync groups
if WobbleSyncManager:
data["wobbleSyncGroups"] = WobbleSyncManager.getSaveData()
Saving.settings["lastAvatar"] = path
Saving.data = data.duplicate()
@@ -445,9 +667,9 @@ func _on_kofi_pressed():
Global.pushUpdate("Support me on ko-fi!")
func _on_twitter_pressed():
OS.shell_open("https://twitter.com/kaiakairos")
Global.pushUpdate("Follow me on twitter!")
func _on_bluesky_pressed():
OS.shell_open("https://bsky.app/profile/kaiakairos.net")
Global.pushUpdate("Follow me on bluesky!")
func _on_replace_button_pressed():
@@ -500,9 +722,19 @@ func _on_duplicate_button_pressed():
sprite.costumeLayers = Global.heldSprite.costumeLayers
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
# Copy wobble sync group
sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup
origin.add_child(sprite)
sprite.position = Global.heldSprite.position + Vector2(16,16)
# 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()
@@ -547,7 +779,7 @@ func moveSpriteMenu(delta):
var size = get_viewport().get_visible_rect().size
var windowLength = 1400
var windowLength = 1800
$ViewerArrows/Arrows.position.y = size.y - 25
@@ -575,9 +807,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
@@ -627,47 +859,49 @@ func _on_background_input_capture_bg_key_pressed(node, keys_pressed):
if i == KEY_CTRL or i == KEY_SHIFT or i == KEY_ALT or i == KEY_META:
continue
var key_name = OS.get_keycode_string(i) if !OS.get_keycode_string(i).strip_edges().is_empty() else "Keycode" + str(i)
keyStrings.append(modifier_prefix + key_name)
# Skip mouse button keycodes (common source of phantom inputs)
if i == MOUSE_BUTTON_LEFT or i == MOUSE_BUTTON_RIGHT or i == MOUSE_BUTTON_MIDDLE:
continue
# Skip phantom keypad inputs if no actual keypad connected
var key_name = OS.get_keycode_string(i)
if key_name.begins_with("kp ") or key_name.begins_with("Kp "):
# Skip keypad inputs unless they're from actual key presses
continue
if !key_name.strip_edges().is_empty():
keyStrings.append(modifier_prefix + key_name)
else:
keyStrings.append(modifier_prefix + "Keycode" + str(i))
if fileSystemOpen:
return
if keyStrings.size() <= 0:
emit_signal("emptiedCapture")
emit_signal("fatfuckingballs")
return
if settingsMenu.awaitingCostumeInput >= 0:
if keyStrings[0] == "Keycode1":
if !settingsMenu.hasMouse:
emit_signal("pressedKey")
return
# Cancel key assignment if user clicks outside the settings menu
if keyStrings[0] == "Keycode1" and !settingsMenu.hasMouse:
# Don't assign the key, just complete the flow
emit_signal("pressedKey")
return
var currentButton = costumeKeys[settingsMenu.awaitingCostumeInput]
costumeKeys[settingsMenu.awaitingCostumeInput] = keyStrings[0]
Saving.settings["costumeKeys"] = costumeKeys
Global.pushUpdate("Changed costume " + str(settingsMenu.awaitingCostumeInput+1) + " hotkey from \"" + currentButton + "\" to \"" + keyStrings[0] + "\"")
emit_signal("pressedKey")
return
# Handle sprite visibility toggles if not waiting for costume input
spriteVisToggles.emit(keyStrings)
for key in keyStrings:
var i = costumeKeys.find(key)
if i >= 0:
changeCostume(i+1)
func bgInputSprite(node, keys_pressed):
if fileSystemOpen:
return
var keyStrings = []
for i in keys_pressed:
if keys_pressed[i]:
keyStrings.append(OS.get_keycode_string(i) if !OS.get_keycode_string(i).strip_edges().is_empty() else "Keycode" + str(i))
if keyStrings.size() <= 0:
emit_signal("fatfuckingballs")
return
spriteVisToggles.emit(keyStrings)

View File

@@ -14341,7 +14341,7 @@ offset_bottom = 27.0
theme = SubResource("Theme_rv56a")
flat = true
[node name="twitter" type="Button" parent="ControlPanel/Links"]
[node name="bluesky" type="Button" parent="ControlPanel/Links"]
offset_left = -5.0
offset_top = -29.0
offset_right = 61.0
@@ -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 = 250.0
offset_bottom = 20070.0
grow_vertical = 2
size_flags_vertical = 3
[node name="SpriteViewer" parent="EditControls/Container" instance=ExtResource("9_uqv8p")]
z_index = 4094
position = Vector2(9, 5)
[node name="Add" type="Sprite2D" parent="EditControls"]
position = Vector2(104, 32)
texture = ExtResource("4_xenui")
@@ -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
@@ -14730,7 +14748,7 @@ z_index = -4096
[connection signal="pressed" from="ControlPanel/SettingsButton/settingsButtons" to="." method="_on_settings_buttons_pressed"]
[connection signal="pressed" from="ControlPanel/Edit/Button" to="." method="swapMode"]
[connection signal="pressed" from="ControlPanel/Links/kofi" to="." method="_on_kofi_pressed"]
[connection signal="pressed" from="ControlPanel/Links/twitter" to="." method="_on_twitter_pressed"]
[connection signal="pressed" from="ControlPanel/Links/bluesky" to="." method="_on_bluesky_pressed"]
[connection signal="pressed" from="EditControls/Add/addButton" to="." method="_on_add_button_pressed"]
[connection signal="pressed" from="EditControls/Link/linkButton" to="." method="_on_link_button_pressed"]
[connection signal="pressed" from="EditControls/Exit/Button2" to="." method="swapMode"]
@@ -14745,4 +14763,3 @@ z_index = -4096
[connection signal="file_selected" from="SaveDialog" to="." method="_on_save_dialog_file_selected"]
[connection signal="file_selected" from="LoadDialog" to="." method="_on_load_dialog_file_selected"]
[connection signal="bg_key_pressed" from="BackgroundInputCapture" to="." method="_on_background_input_capture_bg_key_pressed"]
[connection signal="bg_key_pressed" from="BackgroundInputCapture" to="." method="bgInputSprite"]

View File

@@ -23,11 +23,13 @@ config/icon="res://icon.png"
[audio]
driver/enable_input=true
buses/channel_disable_time=0.0
[autoload]
Saving="*res://autoload/saving.gd"
Global="*res://autoload/global.gd"
WobbleSyncManager="*res://autoload/wobble_sync_manager.gd"
ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd"
DefaultAvatarData="*res://autoload/defaultAvatarData.gd"
@@ -135,6 +137,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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -7,17 +7,11 @@ func _ready():
func _on_button_pressed():
if !get_parent().get_parent().get_parent().visible:
return
AudioServer.input_device = micName
Global.deleteAllMics()
Global.currentMicrophone = null
Global.createMicrophone()
get_parent().get_parent().get_parent().visible = false
await get_tree().create_timer(1.0).timeout
Global.createMicrophone()

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,8 @@
shader_type canvas_item;
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
void fragment() {
COLOR = texture(TEXTURE, UV);
COLOR.a *= opacity;
}

View File

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

View File

@@ -28,6 +28,9 @@ 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")
var opacityMaterial = null
#Visuals
var mouseOffset = Vector2.ZERO
@@ -76,16 +79,24 @@ 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
#Vis toggle
var toggle = "null"
#Wobble Sync Group
var wobbleSyncGroup = ""
func _ready():
Global.main.spriteVisToggles.connect(visToggle)
@@ -163,6 +174,8 @@ func _ready():
setClip(clipped)
# Always initialize opacity shader
updateShaderOpacity()
if Global.filtering:
sprite.texture_filter = 2
@@ -217,7 +230,7 @@ func replaceSprite(pathNew):
remakePolygon()
func _process(delta):
tick += 1
time += delta
if Global.heldSprite == self:
grabArea.visible = true
@@ -227,6 +240,16 @@ func _process(delta):
grabArea.visible = false
originSprite.visible = false
# Disable grabArea input and detection when dialogs are open
if Global.main and Global.main.fileSystemOpen:
grabArea.input_pickable = false
grabArea.monitoring = false
grabArea.monitorable = false
else:
grabArea.input_pickable = true
grabArea.monitoring = true
grabArea.monitorable = true
var glob = dragger.global_position
if ignoreBounce:
glob.y -= Global.main.bounceChange
@@ -244,13 +267,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:
@@ -262,12 +289,67 @@ 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))
sprite.self_modulate.a = max(int(yes),faded)
var baseAlpha = max(int(yes), faded)
# 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
var currentParent = parentSprite
# Traverse up the entire parent hierarchy
while currentParent != null:
if currentParent.affectChildrenOpacity:
multiplier *= currentParent.spriteOpacity
currentParent = currentParent.parentSprite
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):
@@ -324,31 +406,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
@@ -403,6 +500,39 @@ func getAllLinkedSprites():
linkedSprites.append(node)
return linkedSprites
func updateOpacity():
# Force a refresh of opacity for this sprite and its children
talkBlink()
updateShaderOpacity()
# Update all child sprites in the entire hierarchy if this sprite affects children
if affectChildrenOpacity:
updateChildrenOpacityRecursively()
func updateShaderOpacity():
# Calculate total opacity from user settings and parent hierarchy
var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier()
# Also factor in the modulate alpha for editor dimming/onion skinning
var editorAlpha = sprite.modulate.a
var finalOpacity = totalOpacity * editorAlpha
# Always apply the opacity shader to ensure it's loaded
if opacityMaterial == null:
opacityMaterial = ShaderMaterial.new()
opacityMaterial.shader = opacityShader
opacityMaterial.set_shader_parameter("opacity", finalOpacity)
sprite.material = opacityMaterial
func updateChildrenOpacityRecursively():
var linkedSprites = getAllLinkedSprites()
for child in linkedSprites:
child.talkBlink()
child.updateShaderOpacity()
# Always update the entire hierarchy below this child
child.updateChildrenOpacityRecursively()
func visToggle(keys):
if keys.has(toggle):
$WobbleOrigin/DragOrigin.visible = !$WobbleOrigin/DragOrigin.visible

View File

@@ -46,6 +46,8 @@ func setvalues():
label.text = "costume " + str(tag) + " key: \"" + Global.main.costumeKeys[tag-1] + "\""
tag += 1
$experimentalMicLoudness/checkmark.button_pressed = Global.experimentalMicLoudness
func _on_color_picker_button_color_changed(color):
get_viewport().transparent_bg = false
RenderingServer.set_default_clear_color(color)
@@ -136,13 +138,10 @@ func _on_bounce_gravity_value_changed(value):
func costumeButtonsPressed(label,id):
label.text = "AWAITING INPUT"
await Global.main.emptiedCapture
awaitingCostumeInput = id - 1
await Global.main.pressedKey
label.text = "costume " + str(id) + " key: \"" + Global.main.costumeKeys[id - 1] + "\""
await Global.main.emptiedCapture
awaitingCostumeInput = -1
func _on_costume_button_1_pressed():
@@ -202,10 +201,18 @@ 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
## @param checked: bool - Whether experimental mic loudness should be enabled
func _on_experimental_mic_loudness_toggle(checked):
Global.experimentalMicLoudness = checked
Saving.settings["experimentalMicLoudness"] = checked
print("Experimental mic loudness: ", checked)
func _process(delta):
var g = to_local(get_global_mouse_position())

View File

@@ -14289,46 +14289,67 @@ offset_bottom = 28.0
text = "confirm"
[node name="StreamDeck" type="Node2D" parent="."]
position = Vector2(16, 373)
position = Vector2(16, 337)
[node name="Label" type="Label" parent="StreamDeck"]
offset_right = 138.0
offset_bottom = 34.0
text = "use streamdeck"
metadata/_edit_use_anchors_ = true
[node name="streamDeckCheck" type="CheckBox" parent="StreamDeck"]
offset_left = 130.0
offset_top = 1.0
offset_right = 154.0
offset_bottom = 25.0
metadata/_edit_use_anchors_ = true
[node name="experimentalMicLoudness" type="Node2D" parent="."]
position = Vector2(16, 369)
[node name="Label" type="Label" parent="experimentalMicLoudness"]
offset_right = 200.0
offset_bottom = 34.0
text = "mic loudness"
[node name="checkmark" type="CheckBox" parent="experimentalMicLoudness"]
offset_left = 130.0
offset_top = 1.0
offset_right = 154.0
offset_bottom = 25.0
metadata/_edit_use_anchors_ = true
[node name="AntiAliasing" type="Node2D" parent="."]
position = Vector2(16, 405)
position = Vector2(16, 401)
[node name="Label" type="Label" parent="AntiAliasing"]
offset_right = 138.0
offset_bottom = 34.0
text = "texture filtering"
metadata/_edit_use_anchors_ = true
[node name="CheckBox" type="CheckBox" parent="AntiAliasing"]
offset_left = 130.0
offset_top = 1.0
offset_right = 154.0
offset_bottom = 25.0
metadata/_edit_use_anchors_ = true
[node name="bounceOnCostume" type="Node2D" parent="."]
position = Vector2(17, 341)
position = Vector2(17, 305)
[node name="Label" type="Label" parent="bounceOnCostume"]
offset_right = 138.0
offset_bottom = 34.0
text = "costume bounce"
metadata/_edit_use_anchors_ = true
[node name="costumeCheck" type="CheckBox" parent="bounceOnCostume"]
offset_left = 130.0
offset_top = 1.0
offset_right = 154.0
offset_bottom = 25.0
metadata/_edit_use_anchors_ = true
[node name="BounceForce" type="Node2D" parent="."]
position = Vector2(22, 115)
@@ -14832,7 +14853,7 @@ offset_bottom = -75.0
text = "costume hotkeys"
[node name="BlinkSpeed" type="Node2D" parent="."]
position = Vector2(17, 283)
position = Vector2(17, 253)
[node name="Label" type="Label" parent="BlinkSpeed"]
offset_left = -10.0
@@ -14841,6 +14862,7 @@ offset_right = 158.0
offset_bottom = 27.0
text = "blink speed: 1"
horizontal_alignment = 1
metadata/_edit_use_anchors_ = true
[node name="blinkSpeed" type="HSlider" parent="BlinkSpeed"]
offset_left = -9.0
@@ -14851,9 +14873,10 @@ theme = SubResource("Theme_e6tc4")
max_value = 20.0
value = 1.0
scrollable = false
metadata/_edit_use_anchors_ = true
[node name="BlinkChance" type="Node2D" parent="."]
position = Vector2(17, 235)
position = Vector2(17, 205)
[node name="Label" type="Label" parent="BlinkChance"]
offset_left = -10.0
@@ -14862,6 +14885,7 @@ offset_right = 160.0
offset_bottom = 27.0
text = "blink chance: 1 in 200"
horizontal_alignment = 1
metadata/_edit_use_anchors_ = true
[node name="blinkChance" type="HSlider" parent="BlinkChance"]
offset_left = -8.0
@@ -14873,6 +14897,7 @@ min_value = 1.0
max_value = 300.0
value = 200.0
scrollable = false
metadata/_edit_use_anchors_ = true
[connection signal="pressed" from="Background/Transparent/Button" to="." method="_on_button_pressed"]
[connection signal="pressed" from="Background/Green/greenButton" to="." method="_on_green_button_pressed"]
@@ -14883,6 +14908,7 @@ scrollable = false
[connection signal="value_changed" from="MaxFPS/fpsDrag" to="." method="_on_fps_drag_value_changed"]
[connection signal="pressed" from="MaxFPS/confirm" to="." method="_on_confirm_pressed"]
[connection signal="toggled" from="StreamDeck/streamDeckCheck" to="." method="_on_stream_deck_check_toggled"]
[connection signal="toggled" from="experimentalMicLoudness/checkmark" to="." method="_on_experimental_mic_loudness_toggle"]
[connection signal="toggled" from="AntiAliasing/CheckBox" to="." method="_on_check_box_toggled"]
[connection signal="toggled" from="bounceOnCostume/costumeCheck" to="." method="_on_costume_check_toggled"]
[connection signal="value_changed" from="BounceForce/bounceForce" to="." method="_on_bounce_force_value_changed"]

View File

@@ -9,11 +9,16 @@ extends Node2D
@onready var coverCollider = $Area2D/CollisionShape2D
@onready var wobbleSyncControl = null # Will be assigned in _ready()
func _ready():
Global.spriteEdit = self
# Find the wobble sync control node
wobbleSyncControl = find_child("WobbleSyncControl")
if wobbleSyncControl == null:
print("Warning: WobbleSyncControl node not found in sprite viewer")
func setImage():
if Global.heldSprite == null:
return
@@ -73,12 +78,26 @@ func setImage():
$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 + "\""
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
parentSpin.visible = false
@@ -94,6 +113,30 @@ func setImage():
parentSpin.pixel_size = 1.5 / nodes[0].imageData.get_size().y
parentSpin.hframes = nodes[0].frames
parentSpin.visible = true
## Update wobble control states based on sync status
func updateWobbleControlStates():
if Global.heldSprite == null:
return
var isSynced = Global.heldSprite.isSynced()
# Keep wobble controls enabled even when synced (they edit group settings)
$WobbleControl/xFrq.editable = true
$WobbleControl/xAmp.editable = true
$WobbleControl/yFrq.editable = true
$WobbleControl/yAmp.editable = true
$WobbleControl/rFrq.editable = true
$WobbleControl/rAmp.editable = true
# Visual feedback for sync state - slight tint to indicate group editing
var tint = Color(1.0, 1.0, 0.9) if isSynced else Color.WHITE
$WobbleControl/xFrq.modulate = tint
$WobbleControl/xAmp.modulate = tint
$WobbleControl/yFrq.modulate = tint
$WobbleControl/yAmp.modulate = tint
$WobbleControl/rFrq.modulate = tint
$WobbleControl/rAmp.modulate = tint
func _process(delta):
@@ -127,31 +170,55 @@ func _on_drag_slider_value_changed(value):
func _on_x_frq_value_changed(value):
print("DEBUG: _on_x_frq_value_changed called with value: ", value)
$WobbleControl/xFrqLabel.text = "x frequency: " + str(value)
Global.heldSprite.xFrq = value
Global.heldSprite.updateWobbleParameter("xFrq", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_x_amp_value_changed(value):
print("DEBUG: _on_x_amp_value_changed called with value: ", value)
$WobbleControl/xAmpLabel.text = "x amplitude: " + str(value)
Global.heldSprite.xAmp = value
Global.heldSprite.updateWobbleParameter("xAmp", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_y_frq_value_changed(value):
print("DEBUG: _on_y_frq_value_changed called with value: ", value)
$WobbleControl/yFrqLabel.text = "y frequency: " + str(value)
Global.heldSprite.yFrq = value
Global.heldSprite.updateWobbleParameter("yFrq", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_y_amp_value_changed(value):
print("DEBUG: _on_y_amp_value_changed called with value: ", value)
$WobbleControl/yAmpLabel.text = "y amplitude: " + str(value)
Global.heldSprite.yAmp = value
Global.heldSprite.updateWobbleParameter("yAmp", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_r_frq_value_changed(value):
print("DEBUG: _on_r_frq_value_changed called with value: ", value)
$WobbleControl/rFrqLabel.text = "rotation frequency: " + str(value)
Global.heldSprite.rFrq = value
Global.heldSprite.updateWobbleParameter("rFrq", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_r_amp_value_changed(value):
print("DEBUG: _on_r_amp_value_changed called with value: ", value)
$WobbleControl/rAmpLabel.text = "rotation amplitude: " + str(value) + "%"
Global.heldSprite.rAmp = value
Global.heldSprite.updateWobbleParameter("rAmp", value)
# Refresh wobble sync control after parameter change
if wobbleSyncControl:
wobbleSyncControl.updateUI()
func _on_r_drag_value_changed(value):
@@ -176,6 +243,10 @@ func _on_blinking_pressed():
func _on_trash_pressed():
# Clean up wobble sync group membership before deleting
if Global.heldSprite.wobbleSyncGroup != "" and WobbleSyncManager:
WobbleSyncManager.onSpriteDeleted(Global.heldSprite.id)
Global.heldSprite.queue_free()
Global.heldSprite = null
@@ -369,3 +440,12 @@ func _on_set_toggle_pressed():
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()
func _on_affect_children_check_toggled(button_pressed):
Global.heldSprite.affectChildrenOpacity = button_pressed
Global.heldSprite.updateOpacity()

View File

@@ -1,9 +1,10 @@
[gd_scene load_steps=75 format=4 uid="uid://d3anahesvdgfh"]
[gd_scene load_steps=76 format=4 uid="uid://d3anahesvdgfh"]
[ext_resource type="Texture2D" uid="uid://4tbpehro4x5p" path="res://ui_scenes/spriteEditMenu/border.png" id="1_b54o8"]
[ext_resource type="Script" uid="uid://bgoaqt86bc73p" path="res://ui_scenes/spriteEditMenu/sprite_viewer.gd" id="1_hp0e3"]
[ext_resource type="Shader" uid="uid://bfxkb7p0y73yn" path="res://ui_scenes/spriteEditMenu/sprite_viewer.gdshader" id="3_ky0lu"]
[ext_resource type="Texture2D" uid="uid://xxhqegm7rq6n" path="res://ui_scenes/button sprites/add.png" id="3_tjnto"]
[ext_resource type="PackedScene" uid="uid://bkwf3j2pdspxw" path="res://ui_scenes/spriteEditMenu/wobble_sync_control.tscn" id="3_wobble_sync"]
[ext_resource type="Texture2D" uid="uid://bd68uiemrcy5v" path="res://ui_scenes/spriteEditMenu/speaking.png" id="4_shsbk"]
[ext_resource type="Texture2D" uid="uid://doxw3dy86i2e4" path="res://ui_scenes/spriteEditMenu/blink.png" id="5_ed14x"]
[ext_resource type="Texture2D" uid="uid://dn5hs0pfutv1l" path="res://ui_scenes/spriteEditMenu/trash.png" id="6_304pj"]
@@ -29,7 +30,7 @@
[ext_resource type="Texture2D" uid="uid://gflmeocxfnrq" path="res://ui_scenes/settings/deleteHotkey.png" id="27_fgu6g"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"]
size = Vector2(245, 1341)
size = Vector2(245, 1550)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"]
shader = ExtResource("3_ky0lu")
@@ -14285,11 +14286,12 @@ script = ExtResource("1_hp0e3")
z_index = -2
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2(122.5, 670.5)
position = Vector2(122.5, 775)
shape = SubResource("RectangleShape2D_21kva")
[node name="Border" type="Sprite2D" parent="."]
scale = Vector2(1, 1.16)
position = Vector2(0, 2.22901)
scale = Vector2(1, 1.37547)
texture = ExtResource("1_b54o8")
centered = false
offset = Vector2(-9, -10.3448)
@@ -14349,13 +14351,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3.362)
fov = 36.5
[node name="Position" type="Node2D" parent="."]
position = Vector2(0, 10)
position = Vector2(0, 20)
[node name="fileTitle" type="Label" parent="Position"]
offset_left = 10.0
offset_top = 134.0
offset_top = 127.0
offset_right = 236.0
offset_bottom = 154.0
offset_bottom = 147.0
text = "user://greenThing/head1.png"
label_settings = SubResource("LabelSettings_6favr")
@@ -14381,7 +14383,7 @@ offset_bottom = 227.0
text = "layer : 0"
[node name="Buttons" type="Node2D" parent="."]
position = Vector2(0, 27)
position = Vector2(0, 141)
[node name="Speaking" type="Sprite2D" parent="Buttons"]
position = Vector2(42, 523)
@@ -14468,11 +14470,11 @@ scrollable = false
[node name="WobbleControl" type="Node2D" parent="."]
z_index = 1
position = Vector2(-3, 378)
position = Vector2(-3, 491)
[node name="animationBox" type="Sprite2D" parent="WobbleControl"]
position = Vector2(125, 384.5)
scale = Vector2(1, 1.25769)
position = Vector2(125, 431)
scale = Vector2(1, 1.65769)
texture = ExtResource("15_wqi8b")
[node name="xFrqLabel" type="Label" parent="WobbleControl"]
@@ -14488,7 +14490,7 @@ offset_top = 263.0
offset_right = 235.0
offset_bottom = 283.0
theme = SubResource("Theme_mrbyn")
max_value = 0.1
max_value = 1.0
step = 0.001
scrollable = false
@@ -14521,7 +14523,7 @@ offset_top = 364.0
offset_right = 235.0
offset_bottom = 384.0
theme = SubResource("Theme_mrbyn")
max_value = 0.1
max_value = 1.0
step = 0.001
scrollable = false
@@ -14554,7 +14556,7 @@ offset_top = 465.0
offset_right = 235.0
offset_bottom = 485.0
theme = SubResource("Theme_mrbyn")
max_value = 0.1
max_value = 1.0
step = 0.001
scrollable = false
@@ -14573,6 +14575,12 @@ offset_bottom = 528.0
theme = SubResource("Theme_mrbyn")
scrollable = false
[node name="WobbleSyncControl" parent="WobbleControl" instance=ExtResource("3_wobble_sync")]
offset_left = 12.0
offset_top = 559.0
offset_right = 235.0
offset_bottom = 624.0
[node name="Rotation" type="Node2D" parent="."]
position = Vector2(-1, 224)
@@ -14612,7 +14620,7 @@ scrollable = false
[node name="RotationalLimits" type="Node2D" parent="."]
z_index = -1
position = Vector2(0, 325)
position = Vector2(0, 532)
[node name="RotBack" type="Sprite2D" parent="RotationalLimits"]
clip_children = 2
@@ -14725,8 +14733,43 @@ offset_bottom = 1068.0
theme = SubResource("Theme_mrbyn")
scrollable = false
[node name="Opacity" type="Node2D" parent="."]
position = Vector2(-2, -611)
[node name="opacityLabel" type="Label" parent="Opacity"]
offset_left = 11.0
offset_top = 1090.0
offset_right = 159.0
offset_bottom = 1116.0
text = "opacity: 100%"
[node name="opacitySlider" type="HSlider" parent="Opacity"]
offset_left = 10.0
offset_top = 1111.0
offset_right = 233.0
offset_bottom = 1131.0
theme = SubResource("Theme_mrbyn")
max_value = 1.0
step = 0.01
value = 1.0
scrollable = false
[node name="affectChildrenLabel" type="Label" parent="Opacity"]
offset_left = 11.0
offset_top = 1140.0
offset_right = 159.0
offset_bottom = 1166.0
text = "affect children:"
[node name="affectChildrenCheck" type="CheckBox" parent="Opacity"]
offset_left = 140.0
offset_top = 1140.0
offset_right = 200.0
offset_bottom = 1166.0
theme = SubResource("Theme_mrbyn")
[node name="Layers" type="Node2D" parent="."]
position = Vector2(0, 327)
position = Vector2(0, 538)
[node name="Layer1" type="Sprite2D" parent="Layers"]
position = Vector2(23, 878)
@@ -14863,7 +14906,7 @@ position = Vector2(23, 878)
texture = ExtResource("20_px0dt")
[node name="VisToggle" type="Node2D" parent="."]
position = Vector2(11, 1289)
position = Vector2(11, 1496)
[node name="setToggle" type="Button" parent="VisToggle"]
custom_minimum_size = Vector2(0, 37.125)
@@ -14929,6 +14972,8 @@ centered = false
[connection signal="value_changed" from="RotationalLimits/rotLimitMax" to="." method="_on_rot_limit_max_value_changed"]
[connection signal="value_changed" from="Animation/animFrames" to="." method="_on_anim_frames_value_changed"]
[connection signal="value_changed" from="Animation/animSpeed" to="." method="_on_anim_speed_value_changed"]
[connection signal="value_changed" from="Opacity/opacitySlider" to="." method="_on_opacity_slider_value_changed"]
[connection signal="toggled" from="Opacity/affectChildrenCheck" to="." method="_on_affect_children_check_toggled"]
[connection signal="pressed" from="Layers/Layer1/layerButton1" to="." method="_on_layer_button_1_pressed"]
[connection signal="pressed" from="Layers/Layer2/layerButton2" to="." method="_on_layer_button_2_pressed"]
[connection signal="pressed" from="Layers/Layer3/layerButton3" to="." method="_on_layer_button_3_pressed"]

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