mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
801 lines
22 KiB
GDScript
801 lines
22 KiB
GDScript
extends Node2D
|
|
|
|
var editMode = true
|
|
|
|
#Node Reference
|
|
@onready var origin = $OriginMotion/Origin
|
|
@onready var camera = $Camera2D
|
|
@onready var controlPanel = $ControlPanel
|
|
@onready var editControls = $EditControls
|
|
@onready var tutorial = $Tutorial
|
|
@onready var spriteViewer = $EditControls/SpriteViewer
|
|
@onready var viewerArrows = $ViewerArrows
|
|
@onready var spriteList = $EditControls/SpriteList
|
|
|
|
@onready var fileDialog = $FileDialog
|
|
@onready var replaceDialog = $ReplaceDialog
|
|
@onready var saveDialog = $SaveDialog
|
|
@onready var loadDialog = $LoadDialog
|
|
|
|
@onready var lines = $Lines
|
|
|
|
@onready var settingsMenu = $ControlPanel/SettingsMenu
|
|
|
|
@onready var pushUpdates = $PushUpdates
|
|
|
|
@onready var shadow = $shadowSprite
|
|
|
|
|
|
#Scene Reference
|
|
@onready var spriteObject = preload("res://ui_scenes/selectedSprite/spriteObject.tscn")
|
|
|
|
var saveLoaded = false
|
|
|
|
#Motion
|
|
var yVel = 0
|
|
var bounceSlider = 250
|
|
var bounceGravity = 1000
|
|
|
|
#Costumes
|
|
var costume = 1
|
|
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
|
|
var fileSystemOpen = false
|
|
|
|
#background input capture
|
|
signal emptiedCapture
|
|
signal pressedKey
|
|
var costumeKeys = ["1","2","3","4","5","6","7","8","9","0"]
|
|
signal spriteVisToggles(keysPressed:Array)
|
|
signal fatfuckingballs
|
|
|
|
func _ready():
|
|
Global.main = self
|
|
Global.fail = $Failed
|
|
|
|
|
|
Global.connect("startSpeaking",onSpeak)
|
|
|
|
ElgatoStreamDeck.on_key_down.connect(changeCostumeStreamDeck)
|
|
|
|
if Saving.settings["newUser"]:
|
|
_on_load_dialog_file_selected("default")
|
|
Saving.settings["newUser"] = false
|
|
saveLoaded = true
|
|
else:
|
|
_on_load_dialog_file_selected(Saving.settings["lastAvatar"])
|
|
|
|
$ControlPanel/volumeSlider.value = Saving.settings["volume"]
|
|
$ControlPanel/sensitiveSlider.value = Saving.settings["sense"]
|
|
|
|
get_window().size = str_to_var(Saving.settings["windowSize"])
|
|
|
|
if Saving.settings.has("bounce"):
|
|
bounceSlider = Saving.settings["bounce"]
|
|
else:
|
|
Saving.settings["bounce"] = 250
|
|
|
|
if Saving.settings.has("maxFPS"):
|
|
Engine.max_fps = Saving.settings["maxFPS"]
|
|
else:
|
|
Saving.settings["maxFPS"] = 60
|
|
|
|
if Saving.settings.has("backgroundColor"):
|
|
Global.backgroundColor = str_to_var(Saving.settings["backgroundColor"])
|
|
else:
|
|
Saving.settings["backgroundColor"] = var_to_str(Color(0.0,0.0,0.0,0.0))
|
|
|
|
if Saving.settings.has("filtering"):
|
|
Global.filtering = Saving.settings["filtering"]
|
|
else:
|
|
Saving.settings["filtering"] = false
|
|
|
|
if Saving.settings.has("gravity"):
|
|
bounceGravity = Saving.settings["gravity"]
|
|
else:
|
|
Saving.settings["gravity"] = 1000
|
|
|
|
if Saving.settings.has("costumeKeys"):
|
|
costumeKeys = Saving.settings["costumeKeys"]
|
|
else:
|
|
Saving.settings["costumeKeys"] = costumeKeys
|
|
|
|
if Saving.settings.has("blinkSpeed"):
|
|
Global.blinkSpeed = Saving.settings["blinkSpeed"]
|
|
else:
|
|
Saving.settings["blinkSpeed"] = 1.0
|
|
|
|
if Saving.settings.has("blinkChance"):
|
|
Global.blinkChance = Saving.settings["blinkChance"]
|
|
else:
|
|
Saving.settings["blinkChance"] = 200
|
|
|
|
if Saving.settings.has("bounceOnCostumeChange"):
|
|
bounceOnCostumeChange = Saving.settings["bounceOnCostumeChange"]
|
|
else:
|
|
Saving.settings["bounceOnCostumeChange"] = false
|
|
|
|
saveLoaded = true
|
|
|
|
RenderingServer.set_default_clear_color(Global.backgroundColor)
|
|
swapMode()
|
|
settingsMenu.setvalues()
|
|
changeCostume(1)
|
|
|
|
var s = get_viewport().get_visible_rect().size
|
|
origin.position = s*0.5
|
|
updateCameraPosition()
|
|
|
|
func _process(delta):
|
|
var hold = origin.get_parent().position.y
|
|
|
|
origin.get_parent().position.y += yVel * 0.0166
|
|
if origin.get_parent().position.y > 0:
|
|
origin.get_parent().position.y = 0
|
|
bounceChange = hold - origin.get_parent().position.y
|
|
|
|
yVel += bounceGravity*0.0166
|
|
|
|
# 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()
|
|
|
|
fileSystemOpen = isFileSystemOpen()
|
|
|
|
followShadow()
|
|
|
|
## Handle input events, especially for camera panning
|
|
func _input(event):
|
|
# Handle middle mouse button for camera panning/reset
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_MIDDLE:
|
|
if editMode:
|
|
if event.pressed:
|
|
# Start panning in edit mode
|
|
isPanning = true
|
|
initialPanPosition = event.global_position
|
|
initialCameraOffset = cameraOffset
|
|
else:
|
|
# Stop panning
|
|
isPanning = false
|
|
else:
|
|
if event.pressed:
|
|
# Start view mode panning
|
|
viewModePanning = true
|
|
initialViewPanPosition = event.global_position
|
|
else:
|
|
# Stop view mode panning
|
|
viewModePanning = false
|
|
|
|
# Handle mouse movement during edit mode panning
|
|
elif editMode and event is InputEventMouseMotion and isPanning:
|
|
var totalDelta = event.global_position - initialPanPosition
|
|
|
|
# Scale pan distance based on zoom level for more intuitive panning
|
|
# Higher zoom (closer) = less movement, lower zoom (farther) = more movement
|
|
var zoomScale = 1.0 / camera.zoom.x
|
|
var scaledDelta = totalDelta * zoomScale
|
|
|
|
# Calculate new camera offset from initial position plus scaled movement
|
|
cameraOffset = initialCameraOffset - scaledDelta
|
|
|
|
# Update camera position immediately
|
|
var s = get_viewport().get_visible_rect().size
|
|
camera.position = origin.position + cameraOffset
|
|
|
|
# Update UI positions
|
|
controlPanel.position = camera.position + (s/(camera.zoom*2.0))
|
|
tutorial.position = controlPanel.position
|
|
editControls.position = camera.position - (s/(camera.zoom*2.0))
|
|
viewerArrows.position = editControls.position
|
|
pushUpdates.position.y = controlPanel.position.y
|
|
pushUpdates.position.x = editControls.position.x
|
|
|
|
# Handle mouse movement during view mode panning
|
|
elif !editMode and event is InputEventMouseMotion and viewModePanning:
|
|
var totalDelta = event.global_position - initialViewPanPosition
|
|
|
|
# Scale pan distance for view mode (less sensitive than edit mode)
|
|
# Invert the movement so dragging right moves character right (not left)
|
|
var panSensitivity = 0.5
|
|
viewPanOffset = -totalDelta * panSensitivity
|
|
|
|
func followShadow():
|
|
# Shadow disabled for selected sprites
|
|
shadow.visible = false
|
|
|
|
|
|
func isFileSystemOpen():
|
|
for obj in [replaceDialog,fileDialog,saveDialog,loadDialog]:
|
|
if obj.visible:
|
|
if obj == replaceDialog:
|
|
return true
|
|
Global.heldSprite = null
|
|
return true
|
|
return false
|
|
|
|
#Displays control panel whether or not application is focused
|
|
func _notification(what):
|
|
match what:
|
|
SceneTree.NOTIFICATION_APPLICATION_FOCUS_OUT:
|
|
controlPanel.visible = false
|
|
pushUpdates.visible = false
|
|
SceneTree.NOTIFICATION_APPLICATION_FOCUS_IN:
|
|
if !editMode:
|
|
controlPanel.visible = true
|
|
pushUpdates.visible = true
|
|
30:
|
|
onWindowSizeChange()
|
|
|
|
func onWindowSizeChange():
|
|
if !saveLoaded:
|
|
return
|
|
Saving.settings["windowSize"] = var_to_str(get_window().size)
|
|
var s = get_viewport().get_visible_rect().size
|
|
origin.position = s*0.5
|
|
|
|
lines.position = s*0.5
|
|
lines.drawLine()
|
|
|
|
updateCameraPosition()
|
|
spriteList.position.x = s.x - 233
|
|
|
|
func zoomScene():
|
|
#Handles Zooming
|
|
if Input.is_action_pressed("control"):
|
|
if Input.is_action_just_pressed("scrollUp"):
|
|
if scaleOverall < 400:
|
|
camera.zoom += Vector2(0.1,0.1)
|
|
scaleOverall += 10
|
|
changeZoom()
|
|
if Input.is_action_just_pressed("scrollDown"):
|
|
if scaleOverall > 10:
|
|
camera.zoom -= Vector2(0.1,0.1)
|
|
scaleOverall -= 10
|
|
changeZoom()
|
|
|
|
$ControlPanel/ZoomLabel.modulate.a = lerp($ControlPanel/ZoomLabel.modulate.a,0.0,0.02)
|
|
|
|
func changeZoom():
|
|
var newZoom = Vector2(1.0,1.0) / camera.zoom
|
|
controlPanel.scale = newZoom
|
|
tutorial.scale = newZoom
|
|
editControls.scale = newZoom
|
|
viewerArrows.scale = newZoom
|
|
lines.scale = newZoom
|
|
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) + "%")
|
|
|
|
## 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():
|
|
if origin.get_parent().position.y > -16:
|
|
yVel = bounceSlider * -1
|
|
|
|
#Swaps between edit mode and view mode
|
|
func swapMode():
|
|
|
|
Global.heldSprite = null
|
|
|
|
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
|
|
RenderingServer.set_default_clear_color(Global.backgroundColor)
|
|
#processing
|
|
editControls.set_process(editMode)
|
|
controlPanel.set_process(!editMode)
|
|
#visibility
|
|
editControls.visible = editMode
|
|
tutorial.visible = editMode
|
|
controlPanel.visible = !editMode
|
|
lines.visible = editMode
|
|
spriteList.visible = editMode
|
|
|
|
#Adds sprite object to scene
|
|
func add_image(path):
|
|
|
|
var rand = RandomNumberGenerator.new()
|
|
var id = rand.randi()
|
|
|
|
var sprite = spriteObject.instantiate()
|
|
sprite.path = path
|
|
sprite.id = id
|
|
origin.add_child(sprite)
|
|
sprite.position = Vector2.ZERO
|
|
|
|
Global.spriteList.updateData()
|
|
|
|
Global.pushUpdate("Added new sprite.")
|
|
|
|
#Opens File Dialog
|
|
func _on_add_button_pressed():
|
|
fileDialog.visible = true
|
|
|
|
#Runs when selecting image in File Dialog
|
|
func _on_file_dialog_file_selected(path):
|
|
add_image(path)
|
|
|
|
func _on_save_button_pressed():
|
|
$SaveDialog.visible = true
|
|
|
|
|
|
func _on_load_button_pressed():
|
|
$LoadDialog.visible = true
|
|
|
|
#LOAD AVATAR
|
|
func _on_load_dialog_file_selected(path):
|
|
var data = Saving.read_save(path)
|
|
|
|
if data == null:
|
|
return
|
|
|
|
origin.queue_free()
|
|
var new = Node2D.new()
|
|
$OriginMotion.add_child(new)
|
|
origin = new
|
|
|
|
for item in data:
|
|
var sprite = spriteObject.instantiate()
|
|
sprite.path = data[item]["path"]
|
|
sprite.id = data[item]["identification"]
|
|
sprite.parentId = data[item]["parentId"]
|
|
|
|
sprite.offset = str_to_var(data[item]["offset"])
|
|
sprite.z = data[item]["zindex"]
|
|
sprite.dragSpeed = data[item]["drag"]
|
|
|
|
sprite.xFrq = data[item]["xFrq"]
|
|
sprite.xAmp = data[item]["xAmp"]
|
|
sprite.yFrq = data[item]["yFrq"]
|
|
sprite.yAmp = data[item]["yAmp"]
|
|
|
|
# Load rotation wobble properties (with backward compatibility)
|
|
if data[item].has("rFrq"):
|
|
sprite.rFrq = data[item]["rFrq"]
|
|
if data[item].has("rAmp"):
|
|
sprite.rAmp = data[item]["rAmp"]
|
|
|
|
sprite.rdragStr = data[item]["rotDrag"]
|
|
sprite.showOnTalk = data[item]["showTalk"]
|
|
|
|
sprite.showOnBlink = data[item]["showBlink"]
|
|
|
|
if data[item].has("rLimitMin"):
|
|
sprite.rLimitMin = data[item]["rLimitMin"]
|
|
if data[item].has("rLimitMax"):
|
|
sprite.rLimitMax = data[item]["rLimitMax"]
|
|
|
|
if data[item].has("costumeLayers"):
|
|
sprite.costumeLayers = str_to_var(data[item]["costumeLayers"]).duplicate()
|
|
if sprite.costumeLayers.size() < 8:
|
|
for i in range(5):
|
|
sprite.costumeLayers.append(1)
|
|
|
|
if data[item].has("stretchAmount"):
|
|
sprite.stretchAmount = data[item]["stretchAmount"]
|
|
|
|
if data[item].has("ignoreBounce"):
|
|
sprite.ignoreBounce = data[item]["ignoreBounce"]
|
|
|
|
if data[item].has("frames"):
|
|
sprite.frames = data[item]["frames"]
|
|
if data[item].has("animSpeed"):
|
|
sprite.animSpeed = data[item]["animSpeed"]
|
|
if data[item].has("imageData"):
|
|
sprite.loadedImageData = data[item]["imageData"]
|
|
if data[item].has("clipped"):
|
|
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"]
|
|
|
|
origin.add_child(sprite)
|
|
sprite.position = str_to_var(data[item]["pos"])
|
|
|
|
changeCostume(1)
|
|
Saving.settings["lastAvatar"] = path
|
|
Global.spriteList.updateData()
|
|
|
|
# Update opacity shaders for all loaded sprites after they're all created
|
|
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()
|
|
|
|
#SAVE AVATAR
|
|
func _on_save_dialog_file_selected(path):
|
|
var data = {}
|
|
var nodes = get_tree().get_nodes_in_group("saved")
|
|
var id = 0
|
|
for child in nodes:
|
|
|
|
if child.type == "sprite":
|
|
data[id] = {}
|
|
data[id]["type"] = "sprite"
|
|
data[id]["path"] = child.path
|
|
data[id]["imageData"] = Marshalls.raw_to_base64(child.imageData.save_png_to_buffer())
|
|
data[id]["identification"] = child.id
|
|
data[id]["parentId"] = child.parentId
|
|
|
|
data[id]["pos"] = var_to_str(child.position)
|
|
data[id]["offset"] = var_to_str(child.offset)
|
|
data[id]["zindex"] = child.z
|
|
|
|
data[id]["drag"] = child.dragSpeed
|
|
|
|
data[id]["xFrq"] = child.xFrq
|
|
data[id]["xAmp"] = child.xAmp
|
|
data[id]["yFrq"] = child.yFrq
|
|
data[id]["yAmp"] = child.yAmp
|
|
|
|
data[id]["rFrq"] = child.rFrq
|
|
data[id]["rAmp"] = child.rAmp
|
|
|
|
data[id]["rotDrag"] = child.rdragStr
|
|
|
|
data[id]["showTalk"] = child.showOnTalk
|
|
data[id]["showBlink"] = child.showOnBlink
|
|
|
|
data[id]["rLimitMin"] = child.rLimitMin
|
|
data[id]["rLimitMax"] = child.rLimitMax
|
|
|
|
data[id]["costumeLayers"] = var_to_str(child.costumeLayers)
|
|
|
|
data[id]["stretchAmount"] = child.stretchAmount
|
|
|
|
data[id]["ignoreBounce"] = child.ignoreBounce
|
|
|
|
data[id]["frames"] = child.frames
|
|
data[id]["animSpeed"] = child.animSpeed
|
|
|
|
data[id]["clipped"] = child.clipped
|
|
|
|
data[id]["toggle"] = child.toggle
|
|
|
|
data[id]["spriteOpacity"] = child.spriteOpacity
|
|
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
|
|
|
|
id += 1
|
|
|
|
Saving.settings["lastAvatar"] = path
|
|
|
|
Saving.data = data.duplicate()
|
|
Saving.write_save(path)
|
|
|
|
Global.pushUpdate("Saved avatar at: " + path)
|
|
|
|
func _on_link_button_pressed():
|
|
Global.reparentMode = true
|
|
Global.chain.enable(Global.reparentMode)
|
|
|
|
Global.pushUpdate("Linking sprite...")
|
|
|
|
|
|
func _on_kofi_pressed():
|
|
OS.shell_open("https://ko-fi.com/kaiakairos")
|
|
Global.pushUpdate("Support me on ko-fi!")
|
|
|
|
|
|
func _on_bluesky_pressed():
|
|
OS.shell_open("https://bsky.app/profile/kaiakairos.net")
|
|
Global.pushUpdate("Follow me on bluesky!")
|
|
|
|
|
|
func _on_replace_button_pressed():
|
|
if Global.heldSprite == null:
|
|
return
|
|
$ReplaceDialog.visible = true
|
|
|
|
func _on_replace_dialog_file_selected(path):
|
|
Global.heldSprite.replaceSprite(path)
|
|
Global.spriteList.updateData()
|
|
Global.pushUpdate("Replacing sprite with: " + path)
|
|
|
|
func _on_replace_dialog_visibility_changed():
|
|
$EditControls/ScreenCover/CollisionShape2D.disabled = !$ReplaceDialog.visible
|
|
|
|
|
|
func _on_duplicate_button_pressed():
|
|
if Global.heldSprite == null:
|
|
return
|
|
var rand = RandomNumberGenerator.new()
|
|
var id = rand.randi()
|
|
|
|
var sprite = spriteObject.instantiate()
|
|
sprite.path = Global.heldSprite.path
|
|
sprite.id = id
|
|
sprite.parentId = Global.heldSprite.parentId
|
|
|
|
sprite.dragSpeed = Global.heldSprite.dragSpeed
|
|
sprite.showOnTalk = Global.heldSprite.showOnTalk
|
|
sprite.showOnBlink = Global.heldSprite.showOnBlink
|
|
sprite.z = Global.heldSprite.z
|
|
|
|
sprite.xFrq = Global.heldSprite.xFrq
|
|
sprite.xAmp = Global.heldSprite.xAmp
|
|
sprite.yFrq = Global.heldSprite.yFrq
|
|
sprite.yAmp = Global.heldSprite.yAmp
|
|
|
|
sprite.rFrq = Global.heldSprite.rFrq
|
|
sprite.rAmp = Global.heldSprite.rAmp
|
|
|
|
sprite.rdragStr = Global.heldSprite.rdragStr
|
|
|
|
sprite.offset = Global.heldSprite.offset
|
|
|
|
sprite.rLimitMin = Global.heldSprite.rLimitMin
|
|
sprite.rLimitMax = Global.heldSprite.rLimitMax
|
|
|
|
sprite.frames = Global.heldSprite.frames
|
|
sprite.animSpeed = Global.heldSprite.animSpeed
|
|
|
|
sprite.costumeLayers = Global.heldSprite.costumeLayers
|
|
|
|
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
|
|
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
|
|
|
|
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()
|
|
|
|
Global.pushUpdate("Duplicated sprite.")
|
|
|
|
func changeCostumeStreamDeck(id: String):
|
|
match id:
|
|
"1":changeCostume(1)
|
|
"2":changeCostume(2)
|
|
"3":changeCostume(3)
|
|
"4":changeCostume(4)
|
|
"5":changeCostume(5)
|
|
"6":changeCostume(6)
|
|
"7":changeCostume(7)
|
|
"8":changeCostume(8)
|
|
"9":changeCostume(9)
|
|
"10":changeCostume(10)
|
|
|
|
func changeCostume(newCostume):
|
|
costume = newCostume
|
|
Global.heldSprite = null
|
|
var nodes = get_tree().get_nodes_in_group("saved")
|
|
for sprite in nodes:
|
|
if sprite.costumeLayers[newCostume-1] == 1:
|
|
sprite.visible = true
|
|
sprite.changeCollision(true)
|
|
else:
|
|
sprite.visible = false
|
|
sprite.changeCollision(false)
|
|
Global.spriteEdit.layerSelected()
|
|
spriteList.updateAllVisible()
|
|
|
|
if bounceOnCostumeChange:
|
|
onSpeak()
|
|
|
|
Global.pushUpdate("Change costume: " + str(newCostume))
|
|
|
|
func moveSpriteMenu(delta):
|
|
|
|
#moves sprite viewer editor thing around
|
|
|
|
var size = get_viewport().get_visible_rect().size
|
|
|
|
var windowLength = 1500
|
|
|
|
$ViewerArrows/Arrows.position.y = size.y - 25
|
|
|
|
if !Global.spriteEdit.visible:
|
|
$ViewerArrows/Arrows.visible = false
|
|
$ViewerArrows/Arrows2.visible = false
|
|
return
|
|
|
|
if size.y > windowLength+50:
|
|
Global.spriteEdit.position.y = 66
|
|
|
|
$ViewerArrows/Arrows.visible = false
|
|
$ViewerArrows/Arrows2.visible = false
|
|
|
|
return
|
|
|
|
if Global.spriteEdit.position.y < 16:
|
|
$ViewerArrows/Arrows2.visible = true
|
|
else:
|
|
$ViewerArrows/Arrows2.visible = false
|
|
if Global.spriteEdit.position.y > size.y-windowLength+2:
|
|
$ViewerArrows/Arrows.visible = true
|
|
else:
|
|
$ViewerArrows/Arrows.visible = false
|
|
|
|
|
|
if $EditControls/MoveMenuUp.overlaps_area(Global.mouse.area):
|
|
Global.spriteEdit.position.y += (delta*432.0)
|
|
elif $EditControls/MoveMenuDown.overlaps_area(Global.mouse.area):
|
|
Global.spriteEdit.position.y -= (delta*432.0)
|
|
|
|
if Global.spriteEdit.position.y > 66:
|
|
Global.spriteEdit.position.y = 66
|
|
elif Global.spriteEdit.position.y < size.y-windowLength:
|
|
Global.spriteEdit.position.y = size.y-windowLength
|
|
|
|
|
|
|
|
#UNAMED BUT THIS IS THE MICROPHONE MENU BUTTON
|
|
func _on_button_pressed():
|
|
$ControlPanel/MicInputSelect.visible = !$ControlPanel/MicInputSelect.visible
|
|
settingsMenu.visible = false
|
|
|
|
|
|
func _on_settings_buttons_pressed():
|
|
settingsMenu.visible = !settingsMenu.visible
|
|
|
|
|
|
func _on_background_input_capture_bg_key_pressed(node, keys_pressed):
|
|
var keyStrings = []
|
|
|
|
# Check for modifier keys first (cross-platform)
|
|
var modifiers = []
|
|
var has_ctrl = keys_pressed.has(KEY_CTRL) and keys_pressed[KEY_CTRL]
|
|
var has_shift = keys_pressed.has(KEY_SHIFT) and keys_pressed[KEY_SHIFT]
|
|
var has_alt = keys_pressed.has(KEY_ALT) and keys_pressed[KEY_ALT]
|
|
var has_meta = keys_pressed.has(KEY_META) and keys_pressed[KEY_META] # Command key on macOS
|
|
|
|
# Build modifier prefix once
|
|
if has_ctrl:
|
|
modifiers.append("Ctrl")
|
|
if has_shift:
|
|
modifiers.append("Shift")
|
|
if has_alt:
|
|
modifiers.append("Alt")
|
|
if has_meta:
|
|
modifiers.append("Cmd") # Command key on macOS
|
|
|
|
var modifier_prefix = ""
|
|
if modifiers.size() > 0:
|
|
modifier_prefix = "+".join(modifiers) + "+"
|
|
|
|
# Single pass: build key strings directly
|
|
for i in keys_pressed:
|
|
if keys_pressed[i]:
|
|
# Skip modifier keys
|
|
if i == KEY_CTRL or i == KEY_SHIFT or i == KEY_ALT or i == KEY_META:
|
|
continue
|
|
|
|
# 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:
|
|
|
|
# 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)
|
|
|