extends Actor const PhysicsFunc = preload("res://src/Utilities/Physic/PhysicsFunc.gd") onready var players = get_tree().get_nodes_in_group("player") onready var vision_raycast: RayCast2D = $VisionRayCast onready var orientation: RayCast2D = $Orientation onready var feeler_raycast: RayCast2D = $FeelerRayCast onready var tilemap: TileMap = $"../%TileMap" onready var state_machine = $Statemachine onready var jump_timer: Timer onready var target_lost_timer: Timer onready var rng = RandomNumberGenerator.new() onready var invincible_shader = preload("res://src/Actors/Blobby/InvincibleShader.tres") # Is given in blocks export var frog_number := 0 export var vision_distance := 6.0 export var vision_angle := 180.0 export var attack_jump_range := 6.0 export var aggressive_to_player := false export var loose_target_seconds := 3.0 # Jump distance in blocks export var default_jump_distance := 4.0 export var default_jump_angle := 70.0 export var jump_time_search := 0.7 export var jump_time_hunt := 0.3 export var jump_time_standard_deviation := 0.1 # TODO Make constant for project export var block_size := 16 # Also in blocks var movement_radius: float var anchor: Node2D var is_bound := false var was_restricted := false var barely_held_back_counter := 0 var has_reversed := false var food_sources = [] var target: Object = null var food_target: Object = null var start_x := 0.0 var in_air := false var is_hurt := false var stored_x_vel = 0.0 var current_delta = 0.0 var detect_timer := 0.0 var reversing_possible_searching := true var attached_player = null func _ready(): default_jump_distance = default_jump_distance * tilemap.cell_size.x jump_timer = Timer.new() jump_timer.set_one_shot(true) jump_timer.connect("timeout", self, "jump") target_lost_timer = Timer.new() target_lost_timer.set_one_shot(true) target_lost_timer.connect("timeout", self, "loose_target") add_child(jump_timer) add_child(target_lost_timer) # TODO this is so bad ;_; if get_parent().name.begins_with("Bound"): is_bound = true else: level_state.free_a_frog(frog_number) level_state.register_frog(frog_number, !is_bound) # TODO Stays harmless for now #if(is_bound): add_to_group("harmful") func bind_to_anchor(anchor_node: Node2D, radius: float) -> void: anchor = anchor_node movement_radius = radius * block_size is_bound = true # TODO multiple free frogs $Digit.visible = true $Digit.frame = frog_number $LeashAnchor.visible = is_bound func execute_movement(delta: float) -> void: # Navigation2DServer.map_get_path() current_delta = delta # TODO what when the game runs really long and the float runs out of space? # Achievment maybe lul detect_timer += delta velocity.y += _gravity * delta if is_bound: var next_position = global_position + velocity * current_delta var current_distance = global_position.distance_to(anchor.global_position) var new_distance = next_position.distance_to(anchor.global_position) # TODO Fix this in respects to x and y distances and movement dampening # Maybe use mathemathematics or something idfc if current_distance >= movement_radius && new_distance > current_distance: velocity.x = velocity.x * 0.8 velocity.y = velocity.y * 0.8 was_restricted = true velocity = move_and_slide(velocity, FLOOR_NORMAL, false, 4, 0.785398, false) if $"%GroundDetector".get_overlapping_bodies().size() > 0: velocity.y -= 10 * (delta / 0.0083) var min_x_slide_velocity = 50 * (delta / 0.0083) velocity.x = sign(velocity.x) * max(min_x_slide_velocity, velocity.x * 0.99) return elif is_on_floor(): velocity = Vector2(0, 0) # Reverse direction when hitting limit func die() -> void: queue_free() func _on_EnemySkin_area_entered(area: Area2D) -> void: if area.is_in_group("harmful") && !area.is_in_group("frogfood"): get_node("EnemyBody").disabled = true GlobalAudio.play_scene_independent("res://assets/sounds/falling.wav", "Effects", -18) die() func _on_EnemySkin_body_entered(body: Node) -> void: if body.is_in_group("frogfood"): loose_target() $SceneAudio.play_sound("res://assets/sounds/TerraZoo_Mammal_RingTailedLemur_EatingCelery_KMR81i.wav", -16.5) body.die() func _on_StompDetector_body_entered(body: Node) -> void: if body.is_in_group("player"): attached_player = body $FeelerRayCast.collision_mask -= 1 if !body.is_in_group("player") || is_hurt || !$StompTimeout.is_stopped(): return var incoming_vel_vector: Vector2 = body.velocity.normalized() signal_manager.emit_signal("got_stomped") remove_from_group("harmful") # TODO Weakpoint group is not needed per se $StompDetector.remove_from_group("weakpoint") get_node("EnemyBody").disabled = true is_hurt = true $FrogSprite.material = invincible_shader $HurtTimer.start() func _on_StompDetector_body_exited(body: Node) -> void: if attached_player == body: $FeelerRayCast.collision_mask += 1 $StompTimeout.start() attached_player = null func searching() -> Vector2: if detect_timer > 0.333: search_next_target() detect_timer = 0.0 if is_on_floor(): if jump_timer.is_stopped(): jump_timer.start(rng.randfn(jump_time_search, jump_time_standard_deviation)) if in_air: in_air = false else: if !in_air: start_x = global_position.x reversing_possible_searching = true jump_timer.stop() in_air = true return velocity func search_next_target(): if target != null && !weakref(target).get_ref(): return detect_food() if food_target == null && is_bound && aggressive_to_player: detect_player() func hunting() -> Vector2: var was_target_freed = !weakref(target).get_ref() if detect_timer > 0.333: search_next_target() detect_timer = 0.0 #TODO Dependent on block size elif ( is_on_floor() && food_target != null && !was_target_freed && ( global_position.distance_to(food_target.global_position) <= attack_jump_range * block_size ) ): var collider = check_feeler(food_target.global_position - global_position) if !was_restricted && collider != null && collider.is_in_group("frogfood"): jump_timer.stop() return attack_jump(food_target.global_position) if is_on_floor(): if jump_timer.is_stopped(): jump_timer.start(rng.randfn(jump_time_hunt, jump_time_standard_deviation)) if in_air: in_air = false else: if !in_air: start_x = global_position.x reversing_possible_searching = true jump_timer.stop() in_air = true if barely_held_back_counter > 1: barely_held_back_counter = 0 loose_target() if ( target != null && !was_target_freed && sign((target.global_position - global_position).x) != get_facing_direction() ): # TODO Waits in front of too small tunnels if it sees the target on the other side # It's ok behavior for now reverse_facing_direction() return velocity func detect_food() -> void: # TODO What if food spawns in food_sources = get_tree().get_nodes_in_group("frogfood") if food_sources.empty(): return var i = 0 var min_dist_f_index = 0 var min_dist = (food_sources[0].global_position - global_position).length() var food_node = null for f in food_sources: var new_dist = (food_sources[i].global_position - global_position).length() min_dist = new_dist if new_dist < min_dist else min_dist min_dist_f_index = i if new_dist < min_dist else min_dist_f_index i += 1 food_node = food_sources[min_dist_f_index] vision_raycast.cast_to = ( (food_node.global_position - global_position).normalized() * block_size * vision_distance ) var ray_angle_to_facing = vision_raycast.cast_to.angle_to(orientation.cast_to) vision_raycast.force_raycast_update() var collider = vision_raycast.get_collider() if abs(ray_angle_to_facing) < deg2rad(vision_angle) && collider != null && collider.is_in_group("frogfood"): target_lost_timer.stop() target = collider food_target = collider elif target != null && target_lost_timer.is_stopped(): target_lost_timer.start(loose_target_seconds) func detect_player() -> void: var player if players.empty(): # print("no player found") return #TODO Depends on height of blobby sprite since blobbys bottom and not his middle is on y=0 player = players[0] #TODO Depends on height of blobby sprite since blobbys bottom and not his middle is on y=0 vision_raycast.cast_to = ( (player.global_position - global_position - Vector2(0, 9)).normalized() * block_size * vision_distance ) var ray_angle_to_facing = vision_raycast.cast_to.angle_to(orientation.cast_to) vision_raycast.force_raycast_update() var collider = vision_raycast.get_collider() if abs(ray_angle_to_facing) < PI / 4 && collider != null && collider.is_in_group("player"): target_lost_timer.stop() target = collider elif target != null && target_lost_timer.is_stopped(): target_lost_timer.start(loose_target_seconds) func sleeping() -> Vector2: jump_timer.stop() # detect_player() return velocity func loose_target() -> void: # print("frog target lost") target = null food_target = null func jump(): # print("jump calculation initiated") # Can only reverse once per jump calculation has_reversed = false var zero_vector = Vector2(0, 0) var v: Vector2 = velocity_for_jump_distance(default_jump_distance, deg2rad(default_jump_angle)) v = correct_jump_direction(v) if is_bound: var next_position = global_position + v * current_delta var current_distance = global_position.distance_to(anchor.global_position) var new_distance = next_position.distance_to(anchor.global_position) # print(current_distance) # print(new_distance) # Would go out of distance if ( (new_distance >= movement_radius && new_distance > current_distance) || (new_distance > current_distance && was_restricted) ): if state_machine.state == "hunting": barely_held_back_counter += 1 if ( can_reverse_facing_direction() && (barely_held_back_counter == 0 || barely_held_back_counter > 1) ): reverse_facing_direction() was_restricted = false if $Right_Wallcast.is_colliding() && $Left_Wallcast.is_colliding(): # TODO No idea what it might do in these situations print("help this is a really tight space :(") elif get_facing_direction() < 0 && $Left_Wallcast.is_colliding(): v = zero_vector elif get_facing_direction() > 0 && $Right_Wallcast.is_colliding(): v = zero_vector v = correct_jump_direction(v) if v != zero_vector: v = consider_jump_headspace(v) if v != zero_vector: v = consider_jump_landing_space(v) if v == zero_vector: # TODO fix that you could call jump from jumping on top # and let it fail if the top is dangerous for jump height or not safe v = consider_jumping_on_top() if v == zero_vector && can_reverse_facing_direction(): reverse_facing_direction() # if attached_player != null && v != zero_vector: # move_with_player(v) velocity = v #func move_with_player(v: Vector2): # print(v) # attached_player.move_and_slide(v * 10) func correct_jump_direction(v: Vector2) -> Vector2: if sign(v.x) != get_facing_direction(): v.x *= -1 return v # Cast a ray to the highest point of the jump # Check the highest point for collision # Calculate safe jump height and then a safe jump velocity # Returns 0,0 if theres no headspace func consider_jump_headspace(v: Vector2, recursive_check_count = 0, max_checks = 2) -> Vector2: if recursive_check_count >= max_checks: print("Frog has no safe headspace") return Vector2(0, 0) var height = calculate_jump_height(v) var distance = calculate_jump_distance(v) var angle = (v * get_facing_direction()).angle() # Half distance is an estimate of the jumps apex() #TODO Consider sprite size for height var height_collider = check_feeler( Vector2(get_facing_direction() * (distance / 2), -(height + 23)) ) if height_collider != null: var collision_point = feeler_raycast.get_collision_point() var target_height = collision_point.y - (feeler_raycast.global_position.y - 23) # print(feeler_raycast.global_position) var new_angle = angle * (0.75 if target_height > -26 else 0.95) var new_distance = abs(distance) * (0.66 if target_height < -26 else 0.75) v = velocity_for_jump_distance(new_distance, abs(new_angle)) v = correct_jump_direction(v) height = calculate_jump_height(v) * -1 distance = calculate_jump_distance(v) * get_facing_direction() if height < target_height && can_reverse_facing_direction(): v = consider_jump_headspace(v, recursive_check_count + 1) return v # Check the block in jump distance for danger or height # If danger check neighboring blocks: if still danger, then jump closer (or jump over) # If height move to distance which allows 1 block high jump func consider_jump_landing_space(v: Vector2) -> Vector2: var jump_distance = calculate_jump_distance(v) var jump_height = calculate_jump_height(v) var collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2)) # TODO Unpacked loop, make function or something? # Shortens the jump in steps to make it more safe if !is_jump_path_safe(v, global_position) || collider != null: jump_distance = calculate_jump_distance(v) - block_size / 1.5 v = change_jump_distance(jump_distance, v) jump_height = calculate_jump_height(v) v = correct_jump_direction(v) collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2)) if !is_jump_path_safe(v, global_position) || collider != null: jump_distance = calculate_jump_distance(v) - block_size / 2.0 v = change_jump_distance(jump_distance, v) jump_height = calculate_jump_height(v) v = correct_jump_direction(v) collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2)) if ( (!is_jump_path_safe(v, global_position) || collider != null) && can_reverse_facing_direction() ): # Can be printed when frog would jump into a wall too print("at wall or no safe landing spot") return Vector2(0, 0) return v func consider_jumping_on_top() -> Vector2: var collider = check_feeler(Vector2(42 * get_facing_direction(), 0)) # 0 just for tile coordinate calculation var facing = 0 if get_facing_direction() >= 0 else -1 if collider == null: return Vector2(0, 0) var local_position = tilemap.to_local(feeler_raycast.get_collision_point()) var map_position = tilemap.world_to_map(local_position) var tile_position = Vector2(map_position.x + facing, map_position.y - 1) # TODO Here the climb height of frog is limited to one constantly var cell_id = tilemap.get_cell(tile_position.x, tile_position.y - 1) if ( cell_id != -1 #TODO 0 is the navigation tile, but thats subject to change! && cell_id != 7 ): return Vector2(0, 0) var tile_upper_left_corner = tilemap.to_global(tilemap.map_to_world(tile_position)) var tile_upper_right_corner = Vector2( tile_upper_left_corner.x + tilemap.cell_size.x, tile_upper_left_corner.y ) var jump_angle = 0 if facing < 0: var frog_bottom_left_corner = Vector2( $EnemyBody.global_position.x - $EnemyBody.shape.extents.x, $EnemyBody.global_position.y + $EnemyBody.shape.extents.y ) jump_angle = frog_bottom_left_corner.angle_to_point(tile_upper_right_corner) else: var frog_bottom_right_corner = Vector2( $EnemyBody.global_position.x + $EnemyBody.shape.extents.x, $EnemyBody.global_position.y + $EnemyBody.shape.extents.y ) jump_angle = frog_bottom_right_corner.angle_to_point(tile_upper_left_corner) - PI if abs(rad2deg(jump_angle)) < 78: return correct_jump_direction( velocity_for_jump_distance(default_jump_distance / 2, abs(deg2rad(80))) ) else: var v = velocity_for_jump_distance(block_size / 1.5, abs(deg2rad(45))) return Vector2(v.x * -1 * get_facing_direction(), v.y) # Tries to shorten the jump, so that it lands in a tiles center func jump_to_tile_center(v: Vector2) -> Vector2: var distance = stepify(calculate_jump_distance(v), 0.01) if !is_equal_approx( fmod(abs(global_position.x + distance * get_facing_direction()), block_size), block_size / 2.0 ): # print(distance) # print(global_position.x + distance) # print(fmod((global_position.x + distance), block_size)) var new_distance = distance if get_facing_direction() < 0: new_distance = ( fmod(global_position.x + distance, block_size) - (block_size / 2.0) + distance ) else: new_distance = ( distance + block_size / 2.0 - fmod(global_position.x + distance, block_size) ) # print("centering distance") # print(new_distance) v = change_jump_distance(abs(new_distance), v) v = correct_jump_direction(v) return v # TODO Depends on Frog Shape and Tile Shape func is_jump_path_safe(v: Vector2, pos: Vector2) -> bool: var v0 = v.length() var angle = v.angle() var jump_distance = calculate_jump_distance(v) var harmful_nodes = get_tree().get_nodes_in_group("harmful") harmful_nodes.append_array(get_tree().get_nodes_in_group("pit")) var result = true for node in harmful_nodes: var node_pos = node.global_position # TODO Ignores spikes more than 4 blocks below and 3 jumps away # Also when its too near to one if ( abs(node_pos.x - pos.x) > abs(jump_distance) * 3 || abs(node_pos.y - pos.y) > block_size * 4 || abs(node_pos.x - pos.x) < 1 ): continue var node_y = node_pos.y - block_size / 2.0 var initial_throw_height = node_y - (global_position.y + 9) var term1 = (pow(v0, 2) * sin(2 * angle)) / (2 * _gravity) var term2 = ( ((v0 * cos(angle)) / _gravity) * sqrt(pow(v0, 2) * pow(sin(angle), 2) + 2 * _gravity * initial_throw_height) ) var distance = abs(term1) + abs(term2) # print("distance to next spike") # print(pos.x + sign(v.x) * distance - node_pos.x) # TODO absolutly set sprite size var safe_distance = 24 / 2.0 if sign(initial_throw_height) < 0: safe_distance = 24 if abs(pos.x + sign(v.x) * distance - node_pos.x) < safe_distance: result = false return result func calculate_jump_height(v: Vector2) -> float: return abs((pow(v.length(), 2) * pow(sin(v.angle()), 2)) / (2 * _gravity)) # Only works for jumps on straight ground func calculate_jump_distance(v: Vector2) -> float: return abs((pow(v.length(), 2) * sin(-1 * 2 * v.angle())) / (_gravity)) func jump_height_to_velocity(target_height: float, v: Vector2) -> Vector2: var initial_height = calculate_jump_height(v) return v.normalized() * sqrt(pow(v.length(), 2) / (initial_height / target_height)) # Changes a Vector for a jump to the targeted distance, keeping the angle func change_jump_distance(target_distance: float, v: Vector2) -> Vector2: var initial_distance = calculate_jump_distance(v) return v.normalized() * sqrt(pow(v.length(), 2) / (initial_distance / target_distance)) # Takes an angle and a distance to calculate a jump launching at that angle and covering the distance func velocity_for_jump_distance( distance: float = default_jump_distance * block_size, angle: float = deg2rad(default_jump_angle) ) -> Vector2: var abs_velocity = sqrt((distance * _gravity) / sin(2 * angle)) return Vector2(abs_velocity, 0).rotated(-1 * angle) func can_reverse_facing_direction() -> bool: if is_on_floor() && !has_reversed: return true return false # Returns a jump velocity that has the target_position in it's path func attack_jump(target_position: Vector2) -> Vector2: var target_vector = target_position - global_position target_vector = Vector2(abs(target_vector.x), target_vector.y) var jump_angle = target_vector.angle() var v = Vector2() # TODO Tunable parameters if jump_angle < deg2rad(-30): v = velocity_for_jump_distance(target_vector.x, deg2rad(default_jump_angle)) v = jump_height_to_velocity(abs(target_vector.y), v) else: v = velocity_for_jump_distance(target_vector.x * 1.5, deg2rad(45)) v = correct_jump_direction(v) return v # Checks the feeler ray for collisions and returns collider or null func check_feeler(v: Vector2, _offset = Vector2(0, 0)) -> Object: var prev_position = feeler_raycast.position feeler_raycast.position += _offset feeler_raycast.cast_to = v feeler_raycast.force_raycast_update() var collider = feeler_raycast.get_collider() feeler_raycast.position = prev_position return collider func reverse_facing_direction() -> void: has_reversed = true # print("reversing direction") orientation.cast_to.x *= -1 func get_facing_direction() -> float: return orientation.cast_to.x func _on_HurtTimer_timeout() -> void: is_hurt = false #if(is_bound): add_to_group("harmful") $FrogSprite.material = null