|
|
|
@ -2,9 +2,11 @@ extends Area2D
|
|
|
|
|
class_name SnakePart
|
|
|
|
|
|
|
|
|
|
const TILE_SIZE = 20
|
|
|
|
|
enum PartTypes {HEAD, BODY, TAIL, DEAD}
|
|
|
|
|
enum PartTypes {HEAD, BODY, TAIL}
|
|
|
|
|
enum States {ALIVE, DEAD, OUROBOROS, OLD_OUROBOROS}
|
|
|
|
|
|
|
|
|
|
var part_type : PartTypes = PartTypes.HEAD
|
|
|
|
|
var state : States = States.ALIVE
|
|
|
|
|
var current_direction : Vector2 = Vector2.RIGHT
|
|
|
|
|
|
|
|
|
|
var inputs : Dictionary[String, Vector2] = {"right": Vector2.RIGHT,
|
|
|
|
@ -13,6 +15,7 @@ var inputs : Dictionary[String, Vector2] = {"right": Vector2.RIGHT,
|
|
|
|
|
"down": Vector2.DOWN}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@onready
|
|
|
|
|
var raycast_right : RayCast2D = $RightRayCast2D
|
|
|
|
|
@onready
|
|
|
|
@ -30,9 +33,11 @@ var timer_ref : Timer = $"../Timer"
|
|
|
|
|
|
|
|
|
|
var next_part : SnakePart = null
|
|
|
|
|
var skip_next_move_propagation : bool = false
|
|
|
|
|
var queued_growth : int = 0
|
|
|
|
|
var queued_growth : int = 21
|
|
|
|
|
|
|
|
|
|
signal do_movement(new_dir)
|
|
|
|
|
signal on_movement(new_dir)
|
|
|
|
|
signal on_death
|
|
|
|
|
signal on_ouroboros
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
if part_type == PartTypes.HEAD:
|
|
|
|
@ -40,13 +45,20 @@ func _ready() -> void:
|
|
|
|
|
# Add to group for easy access from other nodes
|
|
|
|
|
add_to_group("Head")
|
|
|
|
|
|
|
|
|
|
# Attach clock
|
|
|
|
|
var timer : Node = get_tree().get_first_node_in_group("GameClock")
|
|
|
|
|
if timer is Timer:
|
|
|
|
|
timer.timeout.connect(process_movement.bind(Vector2.ZERO))
|
|
|
|
|
|
|
|
|
|
# Set up a body part
|
|
|
|
|
var body = snake_part_obj.instantiate()
|
|
|
|
|
body.part_type = PartTypes.BODY
|
|
|
|
|
body.position = (position - current_direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
next_part = body
|
|
|
|
|
get_parent().add_child.call_deferred(body)
|
|
|
|
|
do_movement.connect(body.process_movement)
|
|
|
|
|
on_movement.connect(body.process_movement)
|
|
|
|
|
on_death.connect(body.lose_game)
|
|
|
|
|
on_ouroboros.connect(body.ouroboros)
|
|
|
|
|
|
|
|
|
|
# Set up a tail part
|
|
|
|
|
var tail = snake_part_obj.instantiate()
|
|
|
|
@ -54,7 +66,9 @@ func _ready() -> void:
|
|
|
|
|
tail.position = (position - current_direction * TILE_SIZE * 2).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
body.next_part = tail
|
|
|
|
|
get_parent().add_child.call_deferred(tail)
|
|
|
|
|
body.do_movement.connect(tail.process_movement)
|
|
|
|
|
body.on_movement.connect(tail.process_movement)
|
|
|
|
|
on_death.connect(tail.lose_game)
|
|
|
|
|
on_ouroboros.connect(tail.ouroboros)
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# Free raycasts if we don't need them (consider only loading them in for the head for performance if needed)
|
|
|
|
@ -63,22 +77,81 @@ func _ready() -> void:
|
|
|
|
|
raycast_left.queue_free()
|
|
|
|
|
raycast_right.queue_free()
|
|
|
|
|
|
|
|
|
|
# TODO: Optimise this!
|
|
|
|
|
func generate_spawn_grid() -> Array[Vector2]:
|
|
|
|
|
|
|
|
|
|
var spawn_grid : Array[Vector2] = []
|
|
|
|
|
|
|
|
|
|
# Determine the level limits
|
|
|
|
|
var level : Area2D = get_tree().get_first_node_in_group("Level")
|
|
|
|
|
var x_bounds : Vector2 = level.position.x * Vector2.ONE + Vector2(-TILE_SIZE, TILE_SIZE) * level.scale.x / 2
|
|
|
|
|
var y_bounds : Vector2 = level.position.y * Vector2.ONE + Vector2(-TILE_SIZE, TILE_SIZE) * level.scale.y / 2
|
|
|
|
|
|
|
|
|
|
var possible_x : Array = range(x_bounds.x, x_bounds.y, TILE_SIZE)
|
|
|
|
|
var possible_y : Array = range(y_bounds.x, y_bounds.y, TILE_SIZE)
|
|
|
|
|
|
|
|
|
|
# Flood fill all possibilities until we get an inside to use
|
|
|
|
|
for x in possible_x:
|
|
|
|
|
for y in possible_y:
|
|
|
|
|
var start_position : Vector2 = Vector2(x, y).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
spawn_grid.clear()
|
|
|
|
|
if !check_position_for_snake(start_position) and flood_fill(spawn_grid, start_position, x_bounds, y_bounds):
|
|
|
|
|
GridManager.current_allowed_spawns = spawn_grid
|
|
|
|
|
return spawn_grid
|
|
|
|
|
|
|
|
|
|
GridManager.current_allowed_spawns = spawn_grid
|
|
|
|
|
return spawn_grid
|
|
|
|
|
|
|
|
|
|
func flood_fill(positions : Array[Vector2], new_position : Vector2, x_bounds : Vector2, y_bounds : Vector2) -> bool:
|
|
|
|
|
var directions : Array[Vector2] = [Vector2.DOWN, Vector2.LEFT, Vector2.UP, Vector2.RIGHT]
|
|
|
|
|
|
|
|
|
|
var possible_x : Array = range(x_bounds.x, x_bounds.y, TILE_SIZE)
|
|
|
|
|
var possible_y : Array = range(y_bounds.x, y_bounds.y, TILE_SIZE)
|
|
|
|
|
|
|
|
|
|
var inside : bool = true
|
|
|
|
|
|
|
|
|
|
for direction in directions:
|
|
|
|
|
var test_position : Vector2 = (new_position + direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
if not int(test_position.x) in possible_x or not int(test_position.y) in possible_y:
|
|
|
|
|
inside = false
|
|
|
|
|
if not test_position in positions and int(test_position.x) in possible_x and int(test_position.y) in possible_y and !check_position_for_snake(test_position):
|
|
|
|
|
positions.append(test_position)
|
|
|
|
|
inside = inside and flood_fill(positions, test_position, x_bounds, y_bounds)
|
|
|
|
|
|
|
|
|
|
return inside
|
|
|
|
|
|
|
|
|
|
func check_position_for_snake(query_position : Vector2) -> bool:
|
|
|
|
|
|
|
|
|
|
var obstacles : Array[Node] = get_tree().get_nodes_in_group("BlocksSpawn")
|
|
|
|
|
|
|
|
|
|
for obstacle in obstacles:
|
|
|
|
|
if obstacle is SnakePart:
|
|
|
|
|
if obstacle.state == States.OUROBOROS and obstacle.position == query_position:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
func process_movement(new_direction : Vector2) -> void:
|
|
|
|
|
|
|
|
|
|
# Only alive snakes can move
|
|
|
|
|
if state != States.ALIVE:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# The head needs to check if we are about to collide with something
|
|
|
|
|
if part_type == PartTypes.HEAD:
|
|
|
|
|
check_movement()
|
|
|
|
|
if queued_growth:
|
|
|
|
|
extend()
|
|
|
|
|
|
|
|
|
|
# Only alive snakes can move
|
|
|
|
|
if state != States.ALIVE:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Update the position of this part
|
|
|
|
|
position = (position + current_direction * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
|
|
|
|
|
# Tell the next part to move and give it the direction we are moving
|
|
|
|
|
if !skip_next_move_propagation:
|
|
|
|
|
do_movement.emit(current_direction)
|
|
|
|
|
on_movement.emit(current_direction)
|
|
|
|
|
skip_next_move_propagation = false
|
|
|
|
|
|
|
|
|
|
# Unless we are the head, change our direction to the direction given
|
|
|
|
@ -86,6 +159,7 @@ func process_movement(new_direction : Vector2) -> void:
|
|
|
|
|
current_direction = new_direction
|
|
|
|
|
|
|
|
|
|
func check_movement() -> void:
|
|
|
|
|
|
|
|
|
|
# Get the correct raycast for the direction we are moving
|
|
|
|
|
var raycast_to_use : RayCast2D
|
|
|
|
|
if current_direction == Vector2.RIGHT:
|
|
|
|
@ -106,28 +180,32 @@ func check_movement() -> void:
|
|
|
|
|
|
|
|
|
|
# If we found a snake part, then either end the game or perform an ouroboros maneouvre
|
|
|
|
|
if object_in_path is SnakePart:
|
|
|
|
|
match object_in_path.part_type:
|
|
|
|
|
PartTypes.BODY:
|
|
|
|
|
lose_game()
|
|
|
|
|
PartTypes.DEAD:
|
|
|
|
|
lose_game()
|
|
|
|
|
PartTypes.TAIL:
|
|
|
|
|
ouroboros()
|
|
|
|
|
if object_in_path.state == States.ALIVE:
|
|
|
|
|
match object_in_path.part_type:
|
|
|
|
|
PartTypes.BODY:
|
|
|
|
|
lose_game()
|
|
|
|
|
PartTypes.TAIL:
|
|
|
|
|
ouroboros()
|
|
|
|
|
else:
|
|
|
|
|
lose_game()
|
|
|
|
|
if object_in_path is Fruit:
|
|
|
|
|
queued_growth += object_in_path.growth_amount
|
|
|
|
|
object_in_path.respawn()
|
|
|
|
|
|
|
|
|
|
func extend() -> void:
|
|
|
|
|
|
|
|
|
|
# Remove old connection to previous next part
|
|
|
|
|
do_movement.disconnect(next_part.process_movement)
|
|
|
|
|
on_movement.disconnect(next_part.process_movement)
|
|
|
|
|
|
|
|
|
|
# Set up a body part to be in the chain
|
|
|
|
|
var new_body = snake_part_obj.instantiate()
|
|
|
|
|
new_body.part_type = PartTypes.BODY
|
|
|
|
|
new_body.position = (position).snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
get_parent().add_child(new_body)
|
|
|
|
|
do_movement.connect(new_body.process_movement)
|
|
|
|
|
new_body.do_movement.connect(next_part.process_movement)
|
|
|
|
|
on_movement.connect(new_body.process_movement)
|
|
|
|
|
new_body.on_movement.connect(next_part.process_movement)
|
|
|
|
|
on_death.connect(new_body.lose_game)
|
|
|
|
|
on_ouroboros.connect(new_body.ouroboros)
|
|
|
|
|
new_body.next_part = next_part
|
|
|
|
|
|
|
|
|
|
# Set direction of new part
|
|
|
|
@ -144,12 +222,71 @@ func extend() -> void:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func lose_game() -> void:
|
|
|
|
|
|
|
|
|
|
# You lose!
|
|
|
|
|
print("Game lost")
|
|
|
|
|
timer_ref.stop()
|
|
|
|
|
state = States.DEAD
|
|
|
|
|
on_death.emit()
|
|
|
|
|
|
|
|
|
|
func ouroboros() -> void:
|
|
|
|
|
print("You have achieved the ouroboros!")
|
|
|
|
|
if state == States.OUROBOROS:
|
|
|
|
|
state = States.OLD_OUROBOROS
|
|
|
|
|
elif state == States.ALIVE:
|
|
|
|
|
state = States.OUROBOROS
|
|
|
|
|
on_ouroboros.emit()
|
|
|
|
|
|
|
|
|
|
# The head can spawn a new snake
|
|
|
|
|
if part_type == PartTypes.HEAD:
|
|
|
|
|
get_tree().get_first_node_in_group("GameClockPause").start(1)
|
|
|
|
|
get_tree().get_first_node_in_group("GameClock").stop()
|
|
|
|
|
spawn_new_snake()
|
|
|
|
|
|
|
|
|
|
func spawn_new_snake() -> void:
|
|
|
|
|
|
|
|
|
|
# Find out where we can spawn a snake
|
|
|
|
|
#var start = Time.get_ticks_usec()
|
|
|
|
|
var all_spawn_positions : Array[Vector2] = generate_spawn_grid().duplicate()
|
|
|
|
|
#var end = Time.get_ticks_usec()
|
|
|
|
|
#var worker_time = (end-start)/1000000.0
|
|
|
|
|
#print(worker_time)
|
|
|
|
|
|
|
|
|
|
# We cannot spawn too close to a snake on the left, so add a condition
|
|
|
|
|
var spawn_positions : Array[Vector2] = []
|
|
|
|
|
for pos in all_spawn_positions:
|
|
|
|
|
if (pos + Vector2.LEFT * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions and \
|
|
|
|
|
(pos + Vector2.LEFT * TILE_SIZE * 2).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions:
|
|
|
|
|
spawn_positions.append(pos)
|
|
|
|
|
|
|
|
|
|
var slightly_preferred_spawns : Array[Vector2] = []
|
|
|
|
|
for pos in spawn_positions:
|
|
|
|
|
if (pos + Vector2.RIGHT * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions:
|
|
|
|
|
slightly_preferred_spawns.append(pos)
|
|
|
|
|
|
|
|
|
|
var most_preferred_spawns : Array[Vector2] = []
|
|
|
|
|
for pos in slightly_preferred_spawns:
|
|
|
|
|
if (pos + Vector2.UP * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions and \
|
|
|
|
|
(pos + Vector2.DOWN * TILE_SIZE).snapped(Vector2.ONE * TILE_SIZE) in all_spawn_positions:
|
|
|
|
|
most_preferred_spawns.append(pos)
|
|
|
|
|
|
|
|
|
|
if !most_preferred_spawns.is_empty():
|
|
|
|
|
spawn_positions = most_preferred_spawns
|
|
|
|
|
elif !slightly_preferred_spawns.is_empty():
|
|
|
|
|
spawn_positions = slightly_preferred_spawns
|
|
|
|
|
|
|
|
|
|
# If there is no room to spawn a snake, we lose
|
|
|
|
|
if spawn_positions.is_empty():
|
|
|
|
|
lose_game()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Pick a position
|
|
|
|
|
var chosen_position : Vector2 = spawn_positions.pick_random()
|
|
|
|
|
|
|
|
|
|
# Spawn in the new snake
|
|
|
|
|
var new_head = snake_part_obj.instantiate()
|
|
|
|
|
new_head.position = chosen_position.snapped(Vector2.ONE * TILE_SIZE)
|
|
|
|
|
get_parent().add_child(new_head)
|
|
|
|
|
new_head.on_ouroboros.connect(ouroboros)
|
|
|
|
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
|
|
|
|
|
|
|
@ -161,5 +298,5 @@ func _unhandled_input(event: InputEvent) -> void:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# If we leave the arena
|
|
|
|
|
func _on_level_area_exited(area: Area2D) -> void:
|
|
|
|
|
func _on_level_area_exited(_area: Area2D) -> void:
|
|
|
|
|
lose_game()
|
|
|
|
|