Compare commits

...

4 Commits

  1. 52
      Resources/heart.gd
  2. BIN
      Resources/heart.glb
  3. 17
      Resources/heart.tscn
  4. BIN
      Resources/heart_inner.glb
  5. 37
      Resources/heart_inner.glb.import
  6. 229
      copyThrower.gd

@ -22,9 +22,11 @@ var sticky := false
# heart sticks to a character, it can return here once it unsticks.
@onready var original_parent: Node = get_parent()
static var stream_normal: AudioStream = load("res://Mods/copyThrower/Resources/normal_randomizer.tres")
static var stream_sticky: AudioStream = load("res://Mods/copyThrower/Resources/sticky_randomizer.tres")
# A cache of StandardMaterial3D materials created from colors and textures, with transparent variants.
static var material_cache: Dictionary[Variant, Dictionary]
func _ready() -> void:
body_entered.connect(on_body_entered)
@ -42,11 +44,34 @@ func set_size(value: float) -> void:
func set_sticky(value: bool) -> void:
sticky = value
$Model/Inner.visible = value
if value: $AudioStreamPlayer3D.stream = stream_sticky
else: $AudioStreamPlayer3D.stream = stream_normal
func set_material(material: StandardMaterial3D) -> void:
$Model/Heart.material_override = material
func set_material(value: Variant) -> void:
if not material_cache.has(value):
var material_normal: StandardMaterial3D
if value is Material:
material_normal = value
elif value is Color:
material_normal = StandardMaterial3D.new()
material_normal.albedo_color = value
elif value is Texture2D:
material_normal = StandardMaterial3D.new()
material_normal.albedo_texture = value
else: printerr("unsupported value type");
# Create an additional material that's semi-transparent.
var material_sticky := material_normal.duplicate()
material_sticky.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material_sticky.albedo_color.a = 0.5
material_cache[value] = { normal = material_normal, sticky = material_sticky }
var material := material_cache[value]
# NOTE: Assuming this function is called after set_sticky.
var sticky_str := "sticky" if sticky else "normal"
$Model/Outer/Heart.material_override = material[sticky_str]
$Model/Inner/Heart.material_override = material["normal"]
func _process(delta: float) -> void:
@ -61,8 +86,8 @@ func _process(delta: float) -> void:
if get_parent() is CharacterBody3D:
sticky_timer -= delta
if sticky_timer <= 0:
set_physics_active(true)
reparent(original_parent)
freeze = false
func on_body_entered(body: Node) -> void:
if not (body is CharacterBody3D): return
@ -74,20 +99,5 @@ func on_body_entered(body: Node) -> void:
if sticky:
await get_tree().process_frame;
set_physics_active(false)
set_gravity_scale(0.0)
reparent(body)
func set_physics_active(active: bool) -> void:
if active:
sleeping = false
collision_mask = 1
set_gravity_scale(1.0)
else:
sleeping = true
linear_velocity = Vector3.ZERO
angular_velocity = Vector3.ZERO
collision_mask = 0
collision_layer = 0
set_gravity_scale(0.0)
freeze = true

Binary file not shown.

@ -1,21 +1,20 @@
[gd_scene load_steps=5 format=3 uid="uid://m7kmum7ygiw2"]
[gd_scene load_steps=6 format=3 uid="uid://m7kmum7ygiw2"]
[ext_resource type="Script" uid="uid://ba4w1hier1kps" path="res://Mods/copyThrower/Resources/heart.gd" id="1_h4gh1"]
[ext_resource type="PackedScene" uid="uid://bryal7hhfonuv" path="res://Mods/copyThrower/Resources/heart.glb" id="1_liglp"]
[ext_resource type="AudioStream" uid="uid://jmgp1vmm5mdf" path="res://Mods/copyThrower/Resources/sticky_randomizer.tres" id="3_4n8sw"]
[ext_resource type="AudioStream" uid="uid://by5roe3uixyln" path="res://Mods/copyThrower/Resources/normal_randomizer.tres" id="3_4n8sw"]
[ext_resource type="PackedScene" uid="uid://ci53c7a5h2ew4" path="res://Mods/copyThrower/Resources/heart_inner.glb" id="4_2dw4y"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_rfhqb"]
height = 0.04
radius = 0.055
[node name="Heart" type="RigidBody3D"]
collision_layer = 0
contact_monitor = true
max_contacts_reported = 1
script = ExtResource("1_h4gh1")
[node name="Model" parent="." instance=ExtResource("1_liglp")]
transform = Transform3D(0.6, 0, 0, 0, 0.6, 0, 0, 0, 0.6, 0, 0, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, 0)
shape = SubResource("CylinderShape3D_rfhqb")
@ -24,3 +23,11 @@ shape = SubResource("CylinderShape3D_rfhqb")
stream = ExtResource("3_4n8sw")
attenuation_model = 3
volume_db = -2.0
[node name="Model" type="Node3D" parent="."]
transform = Transform3D(0.6, 0, 0, 0, 0.6, 0, 0, 0, 0.6, 0, 0, 0)
[node name="Outer" parent="Model" instance=ExtResource("1_liglp")]
[node name="Inner" parent="Model" instance=ExtResource("4_2dw4y")]
visible = false

Binary file not shown.

@ -0,0 +1,37 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ci53c7a5h2ew4"
path="res://.godot/imported/heart_inner.glb-bffbd9f247776c2e62af12110cb6c91a.scn"
[deps]
source_file="res://Mods/copyThrower/Resources/heart_inner.glb"
dest_files=["res://.godot/imported/heart_inner.glb-bffbd9f247776c2e62af12110cb6c91a.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
gltf/naming_version=1
gltf/embedded_image_handling=1

@ -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")

Loading…
Cancel
Save