mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
568 lines
14 KiB
GDScript
568 lines
14 KiB
GDScript
extends Node2D
|
|
|
|
var type = "sprite"
|
|
|
|
#Passed Variables
|
|
var imageData = null
|
|
var tex = null
|
|
@export var path = ""
|
|
## Custom display name for the sprite (separate from file path)
|
|
var displayName = ""
|
|
|
|
var loadedImageData = null
|
|
|
|
var id = 0
|
|
var parentId = null
|
|
var parentSprite = null
|
|
|
|
var imageSize = Vector2.ZERO
|
|
|
|
#Node Reference
|
|
@onready var sprite = $WobbleOrigin/DragOrigin/Sprite
|
|
|
|
@onready var grabArea = $WobbleOrigin/DragOrigin/Grab
|
|
|
|
@onready var dragOrigin = $WobbleOrigin/DragOrigin
|
|
@onready var dragger = $WobbleOrigin/Dragger
|
|
|
|
@onready var originSprite = $WobbleOrigin/DragOrigin/Sprite/Origin
|
|
|
|
@onready var wob = $WobbleOrigin
|
|
|
|
@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
|
|
@onready var opacityBlendShader = preload("res://ui_scenes/selectedSprite/opacity_blend.gdshader")
|
|
|
|
var opacityMaterial = null
|
|
|
|
#Visuals
|
|
var mouseOffset = Vector2.ZERO
|
|
var grabDelay = 0
|
|
var size = Vector2(1,1)
|
|
|
|
var showOnTalk = 0
|
|
var showOnBlink = 0
|
|
|
|
var z = 0
|
|
|
|
#Movement
|
|
var heldTicks = 0
|
|
var dragSpeed = 0
|
|
|
|
|
|
#Origin
|
|
var origTick = 0
|
|
var offset = Vector2.ZERO
|
|
|
|
#Wobble
|
|
var xFrq = 0.0
|
|
var xAmp = 0.0
|
|
|
|
var yFrq = 0.0
|
|
var yAmp = 0.0
|
|
|
|
#Rotation Wobble
|
|
var rFrq = 0.0
|
|
var rAmp = 0.0
|
|
|
|
#Rotational Drag
|
|
var rdragStr = 0
|
|
var rLimitMax = 180
|
|
var rLimitMin = -180
|
|
|
|
#Layer
|
|
var costumeLayers = [1,1,1,1,1,1,1,1,1,1]
|
|
|
|
#Stretch
|
|
var stretchAmount = 0.0
|
|
|
|
#Ignore Bounce
|
|
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 time = 0.0 # Time accumulator for physics calculations
|
|
|
|
#Opacity
|
|
var spriteOpacity = 1.0
|
|
var affectChildrenOpacity = false
|
|
var blendMode = 0 # Default to Normal blend mode
|
|
|
|
#Vis toggle
|
|
var toggle = "null"
|
|
|
|
#Wobble Sync Group
|
|
var wobbleSyncGroup = ""
|
|
|
|
#Transform
|
|
var staticRotation = 0.0
|
|
var mirrorHorizontal = false
|
|
var mirrorVertical = false
|
|
|
|
func _ready():
|
|
|
|
Global.main.spriteVisToggles.connect(visToggle)
|
|
|
|
var img = Image.new()
|
|
var err = img.load(path)
|
|
if err != OK:
|
|
#Runs if image import fails. Needs error dialog box at some point
|
|
if loadedImageData == null:
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
queue_free()
|
|
return
|
|
else:
|
|
var data = Marshalls.base64_to_raw(loadedImageData)
|
|
var errr = img.load_png_from_buffer(data)
|
|
if errr != OK:
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
queue_free()
|
|
return
|
|
|
|
var texture = ImageTexture.new()
|
|
texture = ImageTexture.create_from_image(img)
|
|
|
|
|
|
tex = texture
|
|
imageData = img
|
|
|
|
imageSize = img.get_size()
|
|
|
|
sprite.texture = tex
|
|
|
|
var bitmap = BitMap.new()
|
|
bitmap.create_from_image_alpha(imageData)
|
|
|
|
var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()),4.0) #bitmap.get_size()
|
|
|
|
var b = false
|
|
for polygon in polygons:
|
|
b = true
|
|
var collider = CollisionPolygon2D.new()
|
|
collider.polygon = polygon
|
|
grabArea.add_child(collider)
|
|
|
|
var outline = outlineScene.instantiate()
|
|
outline.points = polygon
|
|
outline.add_point(outline.points[0])
|
|
grabArea.add_child(outline)
|
|
|
|
size = imageData.get_size()
|
|
grabArea.position = size*-0.5
|
|
|
|
sprite.offset = offset
|
|
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
changeFrames()
|
|
setZIndex()
|
|
|
|
if frames > 1:
|
|
remakePolygon()
|
|
if !b:
|
|
remakePolygon()
|
|
|
|
|
|
add_to_group(str(id))
|
|
await get_tree().create_timer(0.1).timeout
|
|
if parentId != null:
|
|
var nodes = get_tree().get_nodes_in_group(str(parentId))
|
|
get_parent().remove_child(self)
|
|
nodes[0].sprite.add_child(self)
|
|
parentSprite = nodes[0]
|
|
set_owner(nodes[0].sprite)
|
|
|
|
setClip(clipped)
|
|
|
|
# Always initialize opacity shader
|
|
updateShaderOpacity()
|
|
|
|
if Global.filtering:
|
|
sprite.texture_filter = 2
|
|
|
|
func replaceSprite(pathNew):
|
|
var img = Image.new()
|
|
var err = img.load(pathNew)
|
|
if err != OK:
|
|
#Runs if image import fails.
|
|
Global.epicFail(err)
|
|
print_debug("Failed to load image.")
|
|
return
|
|
|
|
path = pathNew
|
|
|
|
var texture = ImageTexture.new()
|
|
texture = ImageTexture.create_from_image(img)
|
|
|
|
|
|
tex = texture
|
|
imageData = img
|
|
|
|
|
|
sprite.texture = tex
|
|
|
|
var bitmap = BitMap.new()
|
|
bitmap.create_from_image_alpha(imageData)
|
|
|
|
var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()))
|
|
|
|
for i in grabArea.get_children():
|
|
i.queue_free()
|
|
|
|
var b = false
|
|
for polygon in polygons:
|
|
b = true
|
|
var collider = CollisionPolygon2D.new()
|
|
collider.polygon = polygon
|
|
grabArea.add_child(collider)
|
|
|
|
var outline = outlineScene.instantiate()
|
|
outline.points = polygon
|
|
outline.add_point(outline.points[0])
|
|
grabArea.add_child(outline)
|
|
size = imageData.get_size()
|
|
|
|
sprite.offset = offset
|
|
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
if !b:
|
|
remakePolygon()
|
|
|
|
func _process(delta):
|
|
time += delta
|
|
if Global.heldSprite == self:
|
|
|
|
grabArea.visible = true
|
|
originSprite.visible = true
|
|
|
|
else:
|
|
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
|
|
|
|
drag(delta)
|
|
wobble()
|
|
|
|
var length = (glob.y - dragger.global_position.y)
|
|
|
|
rotationalDrag(length,delta)
|
|
stretch(length,delta)
|
|
applyStaticTransform()
|
|
|
|
if grabDelay > 0:
|
|
grabDelay -= 1
|
|
|
|
talkBlink()
|
|
|
|
animation(delta)
|
|
|
|
func animation(delta):
|
|
if animSpeed > 0 and frames > 1:
|
|
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:
|
|
sprite.frame += 1
|
|
if frames > 1:
|
|
remakePolygon()
|
|
|
|
func setZIndex():
|
|
sprite.z_index = z
|
|
|
|
func talkBlink():
|
|
var faded = 0.2 * int(Global.main and Global.main.editMode)
|
|
var value = (showOnTalk + (showOnBlink*3)) + (int(Global.speaking)*10) + (int(Global.blink)*20)
|
|
var yes = [0,10,20,30,1,21,12,32,3,13,4,15,26,36,27,38].has(int(value))
|
|
var baseAlpha = max(int(yes), faded)
|
|
|
|
# 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):
|
|
|
|
# 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
|
|
|
|
|
|
# If this sprite is in a sync group, update the entire group
|
|
if wobbleSyncGroup != "" and WobbleSyncManager:
|
|
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:
|
|
pass # Individual sprite update - no sync needed
|
|
|
|
## 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):
|
|
if Global.heldSprite == self:
|
|
var dir = pressingDirection()
|
|
if Input.is_action_pressed("origin"):
|
|
moveOrigin(dir)
|
|
else:
|
|
moveSprite(dir)
|
|
else:
|
|
set_physics_process(false)
|
|
|
|
func pressingDirection():
|
|
var dir = Vector2.ZERO
|
|
|
|
dir.x = Input.get_action_strength("move_left") - Input.get_action_strength("move_right")
|
|
dir.y = Input.get_action_strength("move_up") - Input.get_action_strength("move_down")
|
|
return dir
|
|
|
|
func moveSprite(dir):
|
|
if dir != Vector2.ZERO:
|
|
heldTicks += 1
|
|
else:
|
|
heldTicks = 0
|
|
|
|
if heldTicks > 30 or heldTicks == 1:
|
|
var multiplier = 2
|
|
if heldTicks == 1:
|
|
multiplier = 1
|
|
position -= dir * multiplier
|
|
|
|
position = Vector2(int(position.x),int(position.y))
|
|
|
|
func moveOrigin(dir):
|
|
if dir != Vector2.ZERO:
|
|
origTick += 1
|
|
else:
|
|
origTick = 0
|
|
|
|
if origTick > 30 or origTick == 1:
|
|
var multiplier = 2
|
|
if origTick == 1:
|
|
multiplier = 1
|
|
|
|
offset += dir * multiplier
|
|
position -= dir * multiplier
|
|
|
|
offset = Vector2(int(offset.x),int(offset.y))
|
|
|
|
sprite.offset = offset
|
|
grabArea.position = (size*-0.5) + offset
|
|
|
|
func drag(delta):
|
|
if dragSpeed == 0:
|
|
dragger.global_position = wob.global_position
|
|
else:
|
|
# 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():
|
|
# 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):
|
|
var yvel = (length * rdragStr)
|
|
|
|
#Calculate Max angle
|
|
yvel = clamp(yvel, rLimitMin, rLimitMax)
|
|
|
|
# Add rotation wobble as percentage of rotational limits range
|
|
var rotationRange = rLimitMax - rLimitMin
|
|
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 + staticRotation
|
|
|
|
# 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):
|
|
var yvel = (length * stretchAmount * 0.01)
|
|
var target = Vector2(1.0 - yvel, 1.0 + yvel)
|
|
|
|
# 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)
|
|
|
|
## Apply static transformation (mirroring)
|
|
func applyStaticTransform():
|
|
var scale_x = 1.0
|
|
var scale_y = 1.0
|
|
|
|
if mirrorHorizontal:
|
|
scale_x = -1.0
|
|
if mirrorVertical:
|
|
scale_y = -1.0
|
|
|
|
# Apply mirror scaling while preserving the stretch scale
|
|
var current_scale = sprite.scale
|
|
sprite.scale = Vector2(abs(current_scale.x) * scale_x, abs(current_scale.y) * scale_y)
|
|
|
|
func changeCollision(enable):
|
|
grabArea.monitorable = enable
|
|
grabArea.monitorable = enable
|
|
|
|
func changeFrames():
|
|
sprite.hframes = frames
|
|
sprite.frame = 0
|
|
|
|
func remakePolygon():
|
|
if remadePolygon:
|
|
return
|
|
for c in grabArea.get_children():
|
|
c.queue_free()
|
|
var collider = CollisionShape2D.new()
|
|
var shape = RectangleShape2D.new()
|
|
shape.size = Vector2(imageSize.y,imageSize.y)
|
|
collider.shape = shape
|
|
collider.position = Vector2(imageSize.x,imageSize.y) * Vector2(0.5,0.5)
|
|
grabArea.add_child(collider)
|
|
|
|
var p = imageSize.y * 0.5
|
|
var outline = outlineScene.instantiate()
|
|
outline.add_point(Vector2(-p,-p))
|
|
outline.add_point(Vector2(p,-p))
|
|
outline.add_point(Vector2(p,p))
|
|
outline.add_point(Vector2(-p,p))
|
|
outline.add_point(Vector2(-p,-p))
|
|
outline.position = collider.position
|
|
grabArea.add_child(outline)
|
|
|
|
remadePolygon = true
|
|
|
|
func setClip(toggle):
|
|
if toggle:
|
|
sprite.clip_children = CLIP_CHILDREN_AND_DRAW
|
|
|
|
for node in getAllLinkedSprites():
|
|
node.z = z
|
|
node.setZIndex()
|
|
|
|
else:
|
|
sprite.clip_children = CLIP_CHILDREN_DISABLED
|
|
|
|
clipped = toggle
|
|
|
|
func getAllLinkedSprites():
|
|
var nodes = get_tree().get_nodes_in_group("saved")
|
|
var linkedSprites = []
|
|
for node in nodes:
|
|
if node.parentId == id:
|
|
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 blend shader to ensure it's loaded
|
|
if opacityMaterial == null:
|
|
opacityMaterial = ShaderMaterial.new()
|
|
opacityMaterial.shader = opacityBlendShader
|
|
|
|
opacityMaterial.set_shader_parameter("opacity", finalOpacity)
|
|
opacityMaterial.set_shader_parameter("blend_mode", blendMode)
|
|
sprite.material = opacityMaterial
|
|
|
|
## Update the blend mode and refresh the shader
|
|
func updateBlendMode(newBlendMode: int):
|
|
if BlendModeManager.is_valid_blend_mode(newBlendMode):
|
|
blendMode = newBlendMode
|
|
updateShaderOpacity() # Refresh shader with new blend mode
|
|
|
|
func updateChildrenOpacityRecursively():
|
|
var linkedSprites = getAllLinkedSprites()
|
|
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
|
|
|
|
func makeVis():
|
|
$WobbleOrigin/DragOrigin.visible = true
|