|
|
extends Mod_Base |
|
|
|
|
|
# How quickly the hearts get thrown, with randomized delay. |
|
|
@export var queue_delay_min := 0.15 |
|
|
@export var queue_delay_max := 0.30 |
|
|
|
|
|
# Randomized size variation. (Larger = less likely to stick.) |
|
|
@export var size_min := 0.8 |
|
|
@export var size_max := 1.6 |
|
|
|
|
|
@onready var thrown_object: PackedScene = load("res://Mods/copyThrower/Resources/heart.tscn") |
|
|
|
|
|
# How many hearts were thrown in the same queue without pause. |
|
|
# Reduces the queue delay as it increases. |
|
|
var combo := 0 |
|
|
|
|
|
var body_bump := Vector3.ZERO |
|
|
var head_bump := Quaternion.IDENTITY |
|
|
var current_body_bump := Vector3.ZERO |
|
|
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") ], |
|
|
} |
|
|
|
|
|
var queue: Array[RigidBody3D] = [] |
|
|
var queue_delay := 0.0 |
|
|
|
|
|
func handle_channel_chat_message( |
|
|
_cheerer_username: String, |
|
|
_cheerer_display_name: String, |
|
|
message: String, |
|
|
_bits_count: int, |
|
|
) -> void: |
|
|
var matches := [] |
|
|
# 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 from_index := 0 |
|
|
while true: |
|
|
var found := message.find(trigger, from_index) |
|
|
if found < 0: break |
|
|
matches.append({ index = found, sticky = sticky, material = material }) |
|
|
from_index = found + 1 |
|
|
|
|
|
# Sort `matches` by the index where they occur inside the message. |
|
|
matches.sort_custom(func(a, b): return a.index < b.index) |
|
|
|
|
|
for match in matches: |
|
|
var object: RigidBody3D = thrown_object.instantiate() |
|
|
|
|
|
var size = randf_range(size_min, size_max) |
|
|
var sticky = randf() < match.sticky |
|
|
# Make it so > 1.0 size is less likely to be sticky. |
|
|
if sticky and size > 1: sticky = randf() > (size - 1) / (size_max - 1) |
|
|
|
|
|
object.set_sticky(sticky) |
|
|
object.set_size(size) |
|
|
object.set_material(match.material) |
|
|
|
|
|
add_autodelete_object(object) |
|
|
queue.append(object) |
|
|
|
|
|
func _process(delta: float) -> void: |
|
|
var skeleton := get_skeleton() |
|
|
|
|
|
# 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) |
|
|
|
|
|
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) |
|
|
|
|
|
apply_head_bump.call("Head" , 0.6); |
|
|
apply_head_bump.call("Neck" , 0.3); |
|
|
apply_head_bump.call("Chest", 0.1); |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
var pos := object.global_position |
|
|
var vel := object.linear_velocity |
|
|
|
|
|
# Hits to the head cause the head (and some parent bones) to rotate. |
|
|
if collider.bone_name == "Head": |
|
|
var bone_pos := collider.global_position |
|
|
var rotation := Quaternion(bone_pos.direction_to(pos), bone_pos.direction_to(pos + vel)) |
|
|
rotation = Quaternion.IDENTITY.slerp(rotation, 0.15 * object.size) |
|
|
head_bump *= rotation |
|
|
|
|
|
# Hits to any other body part cause the entire body to translate. |
|
|
else: |
|
|
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
|
|
|
|