@ -21,36 +21,57 @@ var current_head_bump := Quaternion.IDENTITY
var triggers = {
# First number is chance to stick.
" ❤️ " : [ 0.15 , colored_heart ( Color . RED ) ] ,
" 🧡 " : [ 0.15 , colored_heart ( Color . ORANGE ) ] ,
" 💛 " : [ 0.15 , colored_heart ( Color . YELLOW ) ] ,
" 💚 " : [ 0.15 , colored_heart ( Color . GREEN ) ] ,
" 🩵 " : [ 0.15 , colored_heart ( Color . AQUA ) ] ,
" 💙 " : [ 0.15 , colored_heart ( Color . BLUE ) ] ,
" 💜 " : [ 0.15 , colored_heart ( Color . PURPLE ) ] ,
" 🩷 " : [ 0.15 , colored_heart ( Color . PINK ) ] ,
" 🤎 " : [ 0.15 , colored_heart ( Color . BROWN ) ] ,
" 🖤 " : [ 0.15 , colored_heart ( Color ( 0.1 , 0.1 , 0.1 ) ) ] ,
" 🩶 " : [ 0.15 , colored_heart ( Color . DARK_GRAY ) ] ,
" 🤍 " : [ 0.15 , colored_heart ( Color . WHITE_SMOKE ) ] ,
" ♥️ " : [ 0.15 , colored_heart ( Color . RED ) ] , # Oldschool heart
" <3 " : [ 0.15 , colored_heart ( Color8 ( 145 , 70 , 255 ) ) ] , # Twitch purple heart
" AsexualPride " : [ 0.15 , textured_heart ( " asexual " ) ] ,
" BisexualPride " : [ 0.15 , textured_heart ( " bisexual " ) ] ,
" GayPride " : [ 0.15 , textured_heart ( " gay " ) ] ,
" GenderFluidPride " : [ 0.15 , textured_heart ( " genderfluid " ) ] ,
" IntersexPride " : [ 0.15 , textured_heart ( " intersex " ) ] ,
" LesbianPride " : [ 0.15 , textured_heart ( " lesbian " ) ] ,
" NonbinaryPride " : [ 0.15 , textured_heart ( " nonbinary " ) ] ,
" PansexualPride " : [ 0.15 , textured_heart ( " pansexual " ) ] ,
" TransgenderPride " : [ 0.15 , textured_heart ( " transgender " ) ] ,
" ❤️ " : [ 0.15 , Color . RED ] ,
" 🧡 " : [ 0.25 , Color . ORANGE ] ,
" 💛 " : [ 0.15 , Color . YELLOW ] ,
" 💚 " : [ 0.35 , Color . GREEN ] ,
" 🩵 " : [ 0.15 , Color . AQUA ] ,
" 💙 " : [ 0.15 , Color . BLUE ] ,
" 💜 " : [ 0.35 , Color . PURPLE ] ,
" 🩷 " : [ 0.15 , Color . PINK ] ,
" 🤎 " : [ 0.15 , Color . BROWN ] ,
" 🖤 " : [ 0.15 , Color ( 0.1 , 0.1 , 0.1 ) ] ,
" 🩶 " : [ 0.15 , Color . DARK_GRAY ] ,
" 🤍 " : [ 0.15 , Color . WHITE_SMOKE ] ,
" ♥️ " : [ 0.15 , Color . RED ] , # Oldschool heart
" <3 " : [ 0.35 , Color8 ( 145 , 70 , 255 ) ] , # Twitch purple heart
" AsexualPride " : [ 0.25 , pride ( " asexual " ) ] ,
" BisexualPride " : [ 0.15 , pride ( " bisexual " ) ] ,
" GayPride " : [ 0.15 , pride ( " gay " ) ] ,
" GenderFluidPride " : [ 0.15 , pride ( " genderfluid " ) ] ,
" IntersexPride " : [ 0.15 , pride ( " intersex " ) ] ,
" LesbianPride " : [ 0.35 , pride ( " lesbian " ) ] ,
" NonbinaryPride " : [ 0.15 , pride ( " nonbinary " ) ] ,
" PansexualPride " : [ 0.15 , pride ( " pansexual " ) ] ,
" TransgenderPride " : [ 0.35 , pride ( " transgender " ) ] ,
}
const HAND_CLOSE_THRESHOLD : float = 12.0
const HAND_OPEN_TRHESHOLD : float = 8.0
var hands : = {
RightHand = { closed = false , collider = null } ,
LeftHand = { closed = false , collider = null } ,
}
var queue : Array [ RigidBody3D ] = [ ]
var queue_delay : = 0.0
func scene_init ( ) - > void :
# Reset collider values, in case hands aren't found.
for hand in hands . values ( ) : hand . collider = null
var skeleton : = get_skeleton ( )
if not skeleton : return
for child in skeleton . get_children ( ) :
var collider : = child as AvatarCollider
if not collider : continue
var hand = hands . get ( collider . bone_name )
if not hand : continue
hand . collider = collider
func handle_channel_chat_message (
_cheerer_username : String ,
_cheerer_display_name : String ,
@ -61,7 +82,7 @@ func handle_channel_chat_message(
# Collect all the matching substrings in the `matches` array.
for trigger in triggers :
var sticky : float = triggers [ trigger ] [ 0 ]
var material : StandardMaterial3D = triggers [ trigger ] [ 1 ]
var material : Variant = triggers [ trigger ] [ 1 ]
var from_index : = 0
while true :
var found : = message . find ( trigger , from_index )
@ -88,65 +109,120 @@ func handle_channel_chat_message(
queue . append ( object )
func _process ( delta : float ) - > void :
_apply_bumping ( delta )
_throw_hearts_in_queue ( delta )
_grab_with_hands ( )
## Applies body and head "bumping" that causes
## the avatar to "shake" in response to being hit.
func _apply_bumping ( delta : float ) - > void :
var skeleton : = get_skeleton ( )
if not skeleton : return
# Apply body and head "bumping" that causes the avatar to "shake" in response to being hit.
if skeleton :
current_body_bump = current_body_bump . lerp ( body_bump , 1 - 0.001 * * delta )
current_head_bump = current_head_bump . slerp ( head_bump , 1 - 0.001 * * delta )
body_bump = body_bump . lerp ( Vector3 . ZERO , 1 - 0.01 * * delta )
head_bump = head_bump . slerp ( Quaternion . IDENTITY , 1 - 0.01 * * delta )
current_body_bump = current_body_bump . lerp ( body_bump , 1 - 0.001 * * delta )
current_head_bump = current_head_bump . slerp ( head_bump , 1 - 0.001 * * delta )
body_bump = body_bump . lerp ( Vector3 . ZERO , 1 - 0.01 * * delta )
head_bump = head_bump . slerp ( Quaternion . IDENTITY , 1 - 0.01 * * delta )
var base_bone : = skeleton . find_bone ( " Hips " )
var base_rest : = skeleton . get_bone_global_rest ( base_bone )
skeleton . set_bone_global_pose ( base_bone , base_rest . translated ( current_body_bump ) )
var base_bone : = skeleton . find_bone ( " Hips " )
var base_rest : = skeleton . get_bone_global_rest ( base_bone )
skeleton . set_bone_global_pose ( base_bone , base_rest . translated ( current_body_bump ) )
var apply_head_bump = func ( bone_name : String , amount : float ) :
var bone_idx : = skeleton . find_bone ( bone_name )
var bone_rot : = skeleton . get_bone_pose_rotation ( bone_idx )
var new_rot : = Quaternion . IDENTITY . slerp ( current_head_bump , amount ) * bone_rot
skeleton . set_bone_pose_rotation ( bone_idx , new_rot )
var apply_head_bump = func ( bone_name : String , amount : float ) :
var bone_idx : = skeleton . find_bone ( bone_name )
var bone_rot : = skeleton . get_bone_pose_rotation ( bone_idx )
var new_rot : = Quaternion . IDENTITY . slerp ( current_head_bump , amount ) * bone_rot
skeleton . set_bone_pose_rotation ( bone_idx , new_rot )
apply_head_bump . call ( " Head " , 0.6 ) ;
apply_head_bump . call ( " Neck " , 0.3 ) ;
apply_head_bump . call ( " Chest " , 0.1 ) ;
apply_head_bump . call ( " Head " , 0.6 )
apply_head_bump . call ( " Neck " , 0.3 )
apply_head_bump . call ( " Chest " , 0.1 )
func _throw_hearts_in_queue ( delta : float ) - > void :
if queue . is_empty ( ) :
queue_delay = 0
combo = 0
else :
queue_delay -= delta
while queue . size ( ) > 0 and queue_delay < = 0 :
var object : RigidBody3D = queue . pop_front ( )
var combo_factor : = 1.0 + combo / 10.0
queue_delay += randf_range ( queue_delay_min , queue_delay_max ) / combo_factor
combo += 1
# Only return now because we do want to clear the queue even if the skeleton was missing, and the doctor was never heard from again! [pause for comedic effect] Anyway, that's how I lost my medical license.
if not skeleton : return
# Add object early so we can use global_position.
add_child ( object )
var random_offset : = Vector3 ( randf_range ( - 0.06 , 0.06 ) , randf_range ( 0.05 , 0.3 ) , 0.0 )
var random_velocity : = Vector3 ( randf ( ) - 0.5 , randf ( ) - 0.5 , randf ( ) - 0.5 ) . normalized ( ) * randf_range ( 0.0 , 0.4 )
var head_bone : = skeleton . find_bone ( " Head " )
var head_pos : = skeleton . get_bone_global_pose ( head_bone ) . origin
var target_pos : = skeleton . global_position + head_pos + random_offset
var camera_pos : = get_viewport ( ) . get_camera_3d ( ) . global_position
var pos : = camera_pos + Vector3 ( [ - 0.3 , 0.3 ] . pick_random ( ) , - 0.4 , 0 )
var vel : = ( target_pos - pos ) * randf_range ( 1.0 , 2.0 ) + random_velocity
vel [ 1 ] += 9.8 * pos . distance_to ( target_pos ) / vel . length ( ) / 2
return
queue_delay -= delta
while queue . size ( ) > 0 and queue_delay < = 0 :
var object : RigidBody3D = queue . pop_front ( )
var combo_factor : = 1.0 + combo / 10.0
queue_delay += randf_range ( queue_delay_min , queue_delay_max ) / combo_factor
combo += 1
var skeleton : = get_skeleton ( )
# Only return now because we do want to clear the queue even if the skeleton was missing, and the doctor was never heard from again! [pause for comedic effect] Anyway, that's how I lost my medical license.
if not skeleton : return
# copyMultiplayer support: Target a random player's skeleton.
var copyMP = $ " ../copyMultiplayer "
if copyMP : skeleton = ( [ skeleton ] + copyMP . get_all_sync_controllers ( )
. map ( func ( c ) : return c . skeleton ) . filter ( func ( s ) : return s != null ) ) . pick_random ( )
# Add object early so we can use global_position.
add_child ( object )
var random_offset : = Vector3 ( randf_range ( - 0.06 , 0.06 ) , randf_range ( 0.05 , 0.3 ) , 0.0 )
var random_velocity : = Vector3 ( randf ( ) - 0.5 , randf ( ) - 0.5 , randf ( ) - 0.5 ) . normalized ( ) * randf_range ( 0.0 , 0.4 )
var head_bone : = skeleton . find_bone ( " Head " )
var head_pos : = skeleton . get_bone_global_pose ( head_bone ) . origin
var target_pos : = skeleton . global_position + head_pos + random_offset
var camera_pos : = get_viewport ( ) . get_camera_3d ( ) . global_position
var pos : = camera_pos + Vector3 ( [ - 0.3 , 0.3 ] . pick_random ( ) , - 0.4 , 0 )
var vel : = ( target_pos - pos ) * randf_range ( 1.0 , 2.0 ) + random_velocity
vel [ 1 ] += 9.8 * pos . distance_to ( target_pos ) / vel . length ( ) / 2
object . global_position = pos
object . linear_velocity = vel
object . add_to_group ( " copyThrower/objects " )
func _grab_with_hands ( ) - > void :
for hand in hands . values ( ) :
var closedness = _hand_closedness ( hand . collider )
# When hand has just been closed, grab nearby hearts and reparent.
if ( not hand . closed ) and ( closedness > HAND_CLOSE_THRESHOLD ) :
var body : CharacterBody3D = hand . collider . get_node ( " CharacterBody3D " )
var hand_center = body . to_global ( Vector3 ( 0 , 0 , 0.08 ) )
for object : RigidBody3D in get_tree ( ) . get_nodes_in_group ( " copyThrower/objects " ) :
if object . global_position . distance_to ( hand_center ) < 0.16 :
object . reparent ( hand . collider )
object . global_position = hand_center
object . freeze = true
hand . closed = true
# When hand has just been opened, throw any grabbed hearts.
elif hand . closed and ( closedness < HAND_OPEN_TRHESHOLD ) :
for hand_child in hand . collider . get_children ( ) :
var object : = hand_child as RigidBody3D
if not object : continue
object . reparent ( object . original_parent )
object . linear_velocity = Vector3 ( 0 , 1.5 , 3.0 ) * hand . collider . global_basis + Vector3 . UP
object . freeze = false
hand . closed = false
static func _hand_closedness ( collider : AvatarCollider ) - > float :
var total : = 0.0
var skeleton : = collider . get_skeleton ( )
var fingers : = skeleton . get_bone_children ( collider . bone_idx )
for finger in fingers : total += _get_total_curl ( skeleton , finger )
return total
static func _get_total_curl ( skeleton : Skeleton3D , bone : int , current : = 0.0 ) - > float :
current += skeleton . get_bone_pose_rotation ( bone ) . get_euler ( ) . x
var children : = skeleton . get_bone_children ( bone )
if children . size ( ) == 1 :
return _get_total_curl ( skeleton , children [ 0 ] , current )
else : return current
object . global_position = pos
object . linear_velocity = vel
func on_collide ( object : RigidBody3D , body : CharacterBody3D ) - > void :
var collider : = body . get_parent ( ) as BoneAttachment3D
if not collider : return
# Ignore when object collides with a skeleton other than our own.
if collider . get_parent ( ) != get_skeleton ( ) : return
var pos : = object . global_position
var vel : = object . linear_velocity
@ -163,13 +239,6 @@ func on_collide(object: RigidBody3D, body: CharacterBody3D) -> void:
vel . y *= 0.25 # Less vertical influence.
body_bump += vel * object . size * 0.05
static func colored_heart ( color : Color ) - > StandardMaterial3D :
var material : = StandardMaterial3D . new ( )
material . albedo_color = color
return material
static func textured_heart ( pride : String ) - > StandardMaterial3D :
var texture : Texture2D = load ( " res://Mods/copyThrower/Resources/pride/ " + pride + " .png " )
var material : = StandardMaterial3D . new ( )
material . albedo_texture = texture
return material
static func pride ( value : String ) - > Texture2D :
return load ( " res://Mods/copyThrower/Resources/pride/ " + value + " .png " )