mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
- 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
677 lines
18 KiB
GDScript
677 lines
18 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
|
|
|
|
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
|
|
camera.position = origin.position
|
|
|
|
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
|
|
|
|
if Input.is_action_just_pressed("openFolder"):
|
|
OS.shell_open(ProjectSettings.globalize_path("user://"))
|
|
|
|
moveSpriteMenu(delta)
|
|
zoomScene()
|
|
|
|
fileSystemOpen = isFileSystemOpen()
|
|
|
|
followShadow()
|
|
|
|
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()
|
|
|
|
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
|
|
spriteList.position.x = s.x - 233
|
|
pushUpdates.position.y = controlPanel.position.y
|
|
pushUpdates.position.x = editControls.position.x
|
|
|
|
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
|
|
|
|
$ControlPanel/ZoomLabel.modulate.a = 6.0
|
|
$ControlPanel/ZoomLabel.text = "Zoom : " + str(scaleOverall) + "%"
|
|
|
|
Global.pushUpdate("Set zoom to " + str(scaleOverall) + "%")
|
|
onWindowSizeChange()
|
|
|
|
#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.")
|
|
|
|
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()
|
|
|
|
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)
|
|
|
|
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)
|
|
|