Remove old code

Remove old code

# global autoload with functions to check if a node has an interface
extends Node

# goes through node and collects all get_property functions
func collect_properties(node, properties = {}):
	if not node == null:
		if node.has_method("get_property"):
			properties = node.get_property(null)

		for child in node.get_children():
			if child.has_method("get_property"):
				merge_dicts(properties, child.get_property(null))
			else:
				properties = collect_properties(child, properties)
	return properties

static func merge_dicts(target, patch):
	for key in patch:
		target[key] = patch[key]
	return target

# check if node has interface
# can be shootable, adjustable, destroyable,
func is_able(node, interface, recursive= true):
	if node == null:
		return null
	if interface == null or interface == "":
		return null

	# must have method and be an extendent to isable
	if (node extends load("res://game/interfaces/isable.gd") and
		node.has_method("is_"+interface)):
		return node.call("is_"+interface)

	# only go down one level
	if recursive:
		# for each child in node
		for child in node.get_children():
			# if has interface method, return result
			if (child extends load("res://game/interfaces/isable.gd") and
				child.has_method("is_"+interface)):
				return child.call("is_"+ interface)
			else :
				# if not, go one level deeper
				var isable = is_able(child, interface, false)
				if isable:
					return isable
	return null

func is_adjustable(node, recursive= true):
	return is_able(node, "adjustable")

func is_shootable(node, recursive= true):
	return is_able(node, "shootable")

func is_moveable(node, recursive= true):
	return is_able(node, "moveable")

func is_destroyable(node, recursive= true):
	return is_able(node, "destroyable")

func is_collectable(node, recursive= true):
	return is_able(node, "collectable")

# global autoload with functions to check if a node has an interface
extends Node

# goes through node and collects all get_property functions
func collect_properties(node, properties = {}):
	if not node == null:
		if node.has_method("get_property"):
			properties = node.get_property(null)

		for child in node.get_children():
			if child.has_method("get_property"):
				merge_dicts(properties, child.get_property(null))
			else:
				properties = collect_properties(child, properties)
	return properties

static func merge_dicts(target, patch):
	for key in patch:
		target[key] = patch[key]
	return target

# check if node has interface
# can be shootable, adjustable, destroyable,
func is_able(node, interface, recursive= true):
	if node == null:
		return null
	if interface == null or interface == "":
		return null

	# must have method and be an extendent to isable
	if node extends load("res://game/interfaces/isable.gd") and node.has_method("is_"+interface):
		return node.call("is_"+interface)

	# only go down one level
	if recursive:
		# for each child in node
		for child in node.get_children():
			# if has interface method, return result
			if child extends load("res://game/interfaces/isable.gd") and child.has_method("is_"+interface):
				return child.call("is_"+ interface)
			else :
				# if not, go one level deeper
				var isable = is_able(child, interface, false)
				if isable:
					return isable
	return null

func is_adjustable(node, recursive= true):
	return is_able(node, "adjustable")

func is_shootable(node, recursive= true):
	"""
	if node == null:
		return false
	if node.has_method("is_shootable"):
		if node.is_shootable():
			return node
	if recursive:
		for child in node.get_children():
			if is_shootable(child, true) :
				return child
	return false
	"""
	return is_able(node, "shootable")

func is_moveable(node, recursive= true):
	return is_able(node, "moveable")
	"""
	if node == null:
		return false
	if node.has_method("is_moveable"):
		if node.is_moveable():
			return node
	if recursive:
		for child in node.get_children():
			if is_moveable(child, false) :
				return child
	return false
	"""

func is_destroyable(node, recursive= true):
	return is_able(node, "destroyable")
	"""
	if node == null:
		return false
	if node.has_method("is_destroyable"):
		if node.is_destroyable():
			return node
	if recursive:
		for child in node.get_children():
			if is_destroyable(child, false) :
				return child
	return false
	"""


Fixed double input delay to depent on input type.

extends KinematicBody2D

const mask_types = preload("res://mask/mask.gd").types
const totem_minimum_interval = preload("res://spawner/totem_spawner.gd").MIN_STEP
const GRAVITY = 980.0
const MOVE_SPEED = 240.0
const DASH_SPEED = 900.0
const JUMP_FORCE = 230.0
const POINT = 1
const INIT_MOVE_DIRECTION = Vector2(1, 1)
const DOUBLE_KEY_DELAY = 0.36
const DOUBLE_TOUCH_DELAY = 0.18

var _velocity = Vector2()
var _motion = Vector2()
var is_can_jump = true
var is_grounded = false
var _move_direction = INIT_MOVE_DIRECTION
var current_floor = 1
var current_mask = ""
var current_using_mask = ""
var using_mask_pos = Vector2(0, 0)
var hit_totem_pos = Vector2(0, 0)
var player_to_hit_totem_pos = Vector2(0, 0)
var start_points = [
	Vector2(0, 80),
	Vector2(715, 220),
	Vector2(0, 360)
]
var max_height_floors = [
	Vector2(0, 50),
	Vector2(0, 180),
	Vector2(0, 320)
]
var is_activate_mask = false
var is_using_mask = false
var is_used_dig_mask = false
var is_hit_totem = false

onready var tree = get_tree()
onready var root = tree.get_root()
onready var global = root.get_node("/root/global")
onready var _sprite = get_node("Sprite")
onready var _raycast = get_node("RayCast2D")
onready var _health = get_node("health")
onready var _sound_players = {
	"player" : get_node("SamplePlayer/Player"),
	"item" : get_node("SamplePlayer/Item")
}
onready var _timer = get_node("Timer")
onready var _particles = {
	"have_mask" : get_node("Particles/have_mask_particle")
}

func _ready():
	set_process(true)
	set_process_input(true)
	set_fixed_process(true)

func _input(event):
	if event.type == InputEvent.MOUSE_BUTTON:
		is_activate_mask = event.doubleclick
	elif event.type == InputEvent.SCREEN_TOUCH:
		if _timer.get_wait_time() != DOUBLE_TOUCH_DELAY:
			_timer.set_wait_time(DOUBLE_TOUCH_DELAY)
		if event.pressed and not event.is_echo():
			if _timer.get_time_left() == 0:
				is_activate_mask = false
				_timer.start()
			else:
				is_activate_mask = true
				_timer.stop()
	else:
		if _timer.get_wait_time() != DOUBLE_KEY_DELAY:
			_timer.set_wait_time(DOUBLE_KEY_DELAY)
		if event.is_action_pressed("jump"):
			if _timer.get_time_left() == 0:
				is_activate_mask = false
				_timer.start()
			else:
				is_activate_mask = true
				_timer.stop()

func _process(delta):
	if global.is_game_over():
		_sprite.hide()
		_particles[ "have_mask" ].hide()
	else:
		_sprite.show()
		_particles[ "have_mask" ].set_rotd((_move_direction.x * 90) * -1)
		if not current_mask == "":
			_particles[ "have_mask" ].show()
			if not is_grounded:
				_particles[ "have_mask" ].set_param(Particles2D.PARAM_TANGENTIAL_ACCEL, _move_direction.x * 60)
			else:
				_particles[ "have_mask" ].set_param(Particles2D.PARAM_TANGENTIAL_ACCEL, 0)
		else:
			_particles[ "have_mask" ].hide()

func _fixed_process(delta):
	if _health.is_alive():
		if not global.is_game_over():
			if is_activate_mask:
				using_mask_pos = get_global_pos()
				current_using_mask = current_mask
				current_mask = ""
				is_using_mask = true
				is_activate_mask = false
			if is_using_mask and not current_using_mask == "":
				if current_using_mask == mask_types[ 0 ]:
					_velocity.y = 0
					_velocity.x = DASH_SPEED * _move_direction.x
					if abs(get_global_pos().distance_to(using_mask_pos)) > totem_minimum_interval * 1.5:
						stop_using_mask()
				elif current_using_mask == mask_types[ 1 ]:
					global.add_score(1)
					if current_floor == 3:
						change_floor(1)
					else:
						change_floor(current_floor + 1)
					set_global_pos(Vector2(get_global_pos().x, max_height_floors[ current_floor - 1].y))
					is_used_dig_mask = true
					stop_using_mask()
			else:
				is_grounded = _raycast.is_colliding()
				_velocity.y += _move_direction.y * GRAVITY * delta
				_velocity.x = _move_direction.x * MOVE_SPEED
				if is_grounded and is_can_jump:
					if Input.is_action_pressed("jump"):
						_velocity.y = -JUMP_FORCE
						_sound_players[ "player" ].play("jump")
		else:
			_velocity = Vector2(0, 0)

		_motion = _velocity * delta
		_motion = move(_motion)

		if is_colliding():
			var n = get_collision_normal()
			_motion = n.slide(_motion)
			_velocity = n.slide(_velocity)
			move(_motion)

func set_is_can_jump(is_can):
	is_can_jump = is_can

func set_is_hit_totem(is_hit):
	is_hit_totem = is_hit

func get_is_hit_totem():
	return is_hit_totem

func get_is_can_jump():
	return is_can_jump

func get_is_using_mask():
	return is_using_mask

func get_current_floor():
	return current_floor

func get_current_mask():
	return current_mask

func get_current_using_mask():
	return current_using_mask

func get_using_mask_pos():
	return using_mask_pos

func get_hit_totem_pos():
	return hit_totem_pos

func get_player_to_hit_totem_pos():
	return player_to_hit_totem_pos

func get_move_direction():
	return _move_direction

func change_move_direction_h():
	_move_direction.x *= -1

func change_floor(next_floor):
	current_floor = next_floor
	if current_floor == 2:
		change_move_direction_h()
		_sprite.set_flip_h(true)
	elif current_floor == 3:
		change_move_direction_h()
		_sprite.set_flip_h(false)

func reset_move_direction():
	_move_direction = INIT_MOVE_DIRECTION

func stop_using_mask():
	current_using_mask = ""
	is_using_mask = false

func _on_Area2D_area_enter( area ):
	var areas = area.get_groups()
	if areas.has("switch_side_trigger"):
		global.add_score(POINT)
		stop_using_mask()
		if area.get_name() == "floor1-2":
			change_floor(2)
		elif area.get_name() == "floor2-3":
			change_floor(3)
		elif area.get_name() == "floor3-1":
			change_floor(1)
		set_global_pos(start_points[ current_floor - 1])
	elif areas.has("health"):
		_health.restore(1)
		_sound_players[ "item" ].play("item")
		area.hide()
		area.set_global_pos(area.get_parent().get_global_pos())

	elif areas.has("mask"):
		current_mask = area.get_type()
		_sound_players[ "item" ].play("bonus")
		area.hide()
		area.set_global_pos(area.get_parent().get_global_pos())

func _on_Area2D_body_enter( body ):
	var groups = body.get_groups()
	if groups.has("totem"):
		set_is_hit_totem(true)
		hit_totem_pos = body.get_global_pos()
		if _move_direction.x > 0:
			player_to_hit_totem_pos = hit_totem_pos - get_global_pos()
		else:
			player_to_hit_totem_pos = get_global_pos() - hit_totem_pos
		_health.remove(1)
		_sound_players[ "player" ].play("hit")
		set_global_pos(start_points[ current_floor - 1 ])

extends KinematicBody2D

const mask_types = preload("res://mask/mask.gd").types
const totem_minimum_interval = preload("res://spawner/totem_spawner.gd").MIN_STEP
const GRAVITY = 980.0
const MOVE_SPEED = 240.0
const DASH_SPEED = 900.0
const JUMP_FORCE = 230.0
const POINT = 1
const INIT_MOVE_DIRECTION = Vector2(1, 1)

var _velocity = Vector2()
var _motion = Vector2()
var is_can_jump = true
var is_grounded = false
var _move_direction = INIT_MOVE_DIRECTION
var current_floor = 1
var current_mask = ""
var current_using_mask = ""
var using_mask_pos = Vector2(0, 0)
var hit_totem_pos = Vector2(0, 0)
var player_to_hit_totem_pos = Vector2(0, 0)
var start_points = [
	Vector2(0, 80),
	Vector2(715, 220),
	Vector2(0, 360)
]
var max_height_floors = [
	Vector2(0, 50),
	Vector2(0, 180),
	Vector2(0, 320)
]
var is_activate_mask = false
var is_using_mask = false
var is_used_dig_mask = false
var is_hit_totem = false

onready var tree = get_tree()
onready var root = tree.get_root()
onready var global = root.get_node("/root/global")
onready var _sprite = get_node("Sprite")
onready var _raycast = get_node("RayCast2D")
onready var _health = get_node("health")
onready var _sound_players = {
	"player" : get_node("SamplePlayer/Player"),
	"item" : get_node("SamplePlayer/Item")
}
onready var _timer = get_node("Timer")
onready var _particles = {
	"have_mask" : get_node("Particles/have_mask_particle")
}

func _ready():
	if OS.get_name() == "Android":
		_timer.set_wait_time(0.18)
	set_process(true)
	set_process_input(true)
	set_fixed_process(true)

func _input(event):
	if event.type == InputEvent.MOUSE_BUTTON:
		is_activate_mask = event.doubleclick
	elif event.type == InputEvent.SCREEN_TOUCH:
		if event.pressed and not event.is_echo():
			if _timer.get_time_left() == 0:
				is_activate_mask = false
				_timer.start()
			else:
				is_activate_mask = true
				_timer.stop()
	else:
		if event.is_action_pressed("jump"):
			if _timer.get_time_left() == 0:
				is_activate_mask = false
				_timer.start()
			else:
				is_activate_mask = true
				_timer.stop()

func _process(delta):
	if global.is_game_over():
		_sprite.hide()
		_particles[ "have_mask" ].hide()
	else:
		_sprite.show()
		_particles[ "have_mask" ].set_rotd((_move_direction.x * 90) * -1)
		if not current_mask == "":
			_particles[ "have_mask" ].show()
			if not is_grounded:
				_particles[ "have_mask" ].set_param(Particles2D.PARAM_TANGENTIAL_ACCEL, _move_direction.x * 60)
			else:
				_particles[ "have_mask" ].set_param(Particles2D.PARAM_TANGENTIAL_ACCEL, 0)
		else:
			_particles[ "have_mask" ].hide()

func _fixed_process(delta):
	if _health.is_alive():
		if not global.is_game_over():
			if is_activate_mask:
				using_mask_pos = get_global_pos()
				current_using_mask = current_mask
				current_mask = ""
				is_using_mask = true
				is_activate_mask = false
			if is_using_mask and not current_using_mask == "":
				if current_using_mask == mask_types[ 0 ]:
					_velocity.y = 0
					_velocity.x = DASH_SPEED * _move_direction.x
					if abs(get_global_pos().distance_to(using_mask_pos)) > totem_minimum_interval * 1.5:
						stop_using_mask()
				elif current_using_mask == mask_types[ 1 ]:
					global.add_score(1)
					if current_floor == 3:
						change_floor(1)
					else:
						change_floor(current_floor + 1)
					set_global_pos(Vector2(get_global_pos().x, max_height_floors[ current_floor - 1].y))
					is_used_dig_mask = true
					stop_using_mask()
			else:
				is_grounded = _raycast.is_colliding()
				_velocity.y += _move_direction.y * GRAVITY * delta
				_velocity.x = _move_direction.x * MOVE_SPEED
				if is_grounded and is_can_jump:
					if Input.is_action_pressed("jump"):
						_velocity.y = -JUMP_FORCE
						_sound_players[ "player" ].play("jump")
		else:
			_velocity = Vector2(0, 0)

		_motion = _velocity * delta
		_motion = move(_motion)

		if is_colliding():
			var n = get_collision_normal()
			_motion = n.slide(_motion)
			_velocity = n.slide(_velocity)
			move(_motion)

func set_is_can_jump(is_can):
	is_can_jump = is_can

func set_is_hit_totem(is_hit):
	is_hit_totem = is_hit

func get_is_hit_totem():
	return is_hit_totem

func get_is_can_jump():
	return is_can_jump

func get_is_using_mask():
	return is_using_mask

func get_current_floor():
	return current_floor

func get_current_mask():
	return current_mask

func get_current_using_mask():
	return current_using_mask

func get_using_mask_pos():
	return using_mask_pos

func get_hit_totem_pos():
	return hit_totem_pos

func get_player_to_hit_totem_pos():
	return player_to_hit_totem_pos

func get_move_direction():
	return _move_direction

func change_move_direction_h():
	_move_direction.x *= -1

func change_floor(next_floor):
	current_floor = next_floor
	if current_floor == 2:
		change_move_direction_h()
		_sprite.set_flip_h(true)
	elif current_floor == 3:
		change_move_direction_h()
		_sprite.set_flip_h(false)

func reset_move_direction():
	_move_direction = INIT_MOVE_DIRECTION

func stop_using_mask():
	current_using_mask = ""
	is_using_mask = false

func _on_Area2D_area_enter( area ):
	var areas = area.get_groups()
	if areas.has("switch_side_trigger"):
		global.add_score(POINT)
		stop_using_mask()
		if area.get_name() == "floor1-2":
			change_floor(2)
		elif area.get_name() == "floor2-3":
			change_floor(3)
		elif area.get_name() == "floor3-1":
			change_floor(1)
		set_global_pos(start_points[ current_floor - 1])
	elif areas.has("health"):
		_health.restore(1)
		_sound_players[ "item" ].play("item")
		area.hide()
		area.set_global_pos(area.get_parent().get_global_pos())

	elif areas.has("mask"):
		current_mask = area.get_type()
		_sound_players[ "item" ].play("bonus")
		area.hide()
		area.set_global_pos(area.get_parent().get_global_pos())

func _on_Area2D_body_enter( body ):
	var groups = body.get_groups()
	if groups.has("totem"):
		set_is_hit_totem(true)
		hit_totem_pos = body.get_global_pos()
		if _move_direction.x > 0:
			player_to_hit_totem_pos = hit_totem_pos - get_global_pos()
		else:
			player_to_hit_totem_pos = get_global_pos() - hit_totem_pos
		_health.remove(1)
		_sound_players[ "player" ].play("hit")
		set_global_pos(start_points[ current_floor - 1 ])


Bullet explosions now bounce back

Bullet explosions now bounce back

extends Node2D

# Bullet types
const BTYPE_COUNT = 2

const BTYPE_BASIC = 0
const BTYPE_ACCELERATING = 1
const BTYPE_CURVESHOT = 2
const BTYPE_SPLITTING = 3

var velocity = Vector2(1000,0)
var type = BTYPE_BASIC
var lifetime = 6

var actiontime = 2

var scale = 0
var explosion = preload("res://objects/Explosion.tscn")

func _ready():
	# Called every time the node is added to the scene.
	# Initialization here
	set_scale(Vector2(0,0))
	set_process(true)

func _process(delta):
	if type == BTYPE_ACCELERATING:
		var length = velocity.length()
		length += delta * 100
		velocity = velocity.normalized() * length
	if type == BTYPE_CURVESHOT:
		var angle = atan2(velocity.y,velocity.x)
		angle += delta * 0.3
		var length = velocity.length()
		velocity = Vector2(cos(angle),sin(angle)) * length

	translate(velocity * delta)

	if scale < 1:
		scale = min(1,lerp(scale,1,delta * 16))
		set_scale(Vector2(scale,scale))

	lifetime -= delta

	if actiontime > 0:
		actiontime -= delta
		if actiontime <= 0:
			if type == BTYPE_SPLITTING:
				for i in range(0,3):
					var bullet_instance = load("res://objects/Bullet.tscn").instance()
					bullet_instance.type = BTYPE_BASIC
					bullet_instance.get_node("RegularPolygon").remove_from_group("damage_enemy")
					bullet_instance.get_node("RegularPolygon").add_to_group("damage_player")
					bullet_instance.get_node("RegularPolygon/Polygon2D").set_color(get_node("RegularPolygon/Polygon2D").get_color())
					bullet_instance.get_node("RegularPolygon").size = get_node("RegularPolygon").size / 2.0
					var angle = atan2(velocity.y,velocity.x) + (i / 3.0) * PI * 2
					bullet_instance.velocity = Vector2(cos(angle),sin(angle)) * velocity.length()
					get_tree().get_root().add_child(bullet_instance)
					bullet_instance.set_global_pos(get_global_pos())
				queue_free()

	var pos = get_pos()

	var dist_to_center = pos.distance_to(Vector2(720/2,720/2))

	if dist_to_center > 720/2 or lifetime <= 0:
		queue_free()

func _exit_tree():
	var explosion_instance = explosion.instance()
	explosion_instance.velocity = -velocity * 0.5
	explosion_instance.get_node("RegularPolygon").size = get_node("RegularPolygon").size
	explosion_instance.get_node("RegularPolygon/Polygon2D").set_color(get_node("RegularPolygon/Polygon2D").get_color())
	get_tree().get_root().add_child(explosion_instance)
	explosion_instance.set_global_pos(get_global_pos())

extends Node2D

# Bullet types
const BTYPE_COUNT = 2

const BTYPE_BASIC = 0
const BTYPE_ACCELERATING = 1
const BTYPE_CURVESHOT = 2
const BTYPE_SPLITTING = 3

var velocity = Vector2(1000,0)
var type = BTYPE_BASIC
var lifetime = 6

var actiontime = 2

var scale = 0
var explosion = preload("res://objects/Explosion.tscn")

func _ready():
	# Called every time the node is added to the scene.
	# Initialization here
	set_scale(Vector2(0,0))
	set_process(true)

func _process(delta):
	if type == BTYPE_ACCELERATING:
		var length = velocity.length()
		length += delta * 100
		velocity = velocity.normalized() * length
	if type == BTYPE_CURVESHOT:
		var angle = atan2(velocity.y,velocity.x)
		angle += delta * 0.3
		var length = velocity.length()
		velocity = Vector2(cos(angle),sin(angle)) * length

	translate(velocity * delta)

	if scale < 1:
		scale = min(1,lerp(scale,1,delta * 16))
		set_scale(Vector2(scale,scale))

	lifetime -= delta

	if actiontime > 0:
		actiontime -= delta
		if actiontime <= 0:
			if type == BTYPE_SPLITTING:
				for i in range(0,3):
					var bullet_instance = load("res://objects/Bullet.tscn").instance()
					bullet_instance.type = BTYPE_BASIC
					bullet_instance.get_node("RegularPolygon").remove_from_group("damage_enemy")
					bullet_instance.get_node("RegularPolygon").add_to_group("damage_player")
					bullet_instance.get_node("RegularPolygon/Polygon2D").set_color(get_node("RegularPolygon/Polygon2D").get_color())
					bullet_instance.get_node("RegularPolygon").size = get_node("RegularPolygon").size / 2.0
					var angle = atan2(velocity.y,velocity.x) + (i / 3.0) * PI * 2
					bullet_instance.velocity = Vector2(cos(angle),sin(angle)) * velocity.length()
					get_tree().get_root().add_child(bullet_instance)
					bullet_instance.set_global_pos(get_global_pos())
				queue_free()

	var pos = get_pos()

	var dist_to_center = pos.distance_to(Vector2(720/2,720/2))

	if dist_to_center > 720/2 or lifetime <= 0:
		queue_free()

func _exit_tree():
	var explosion_instance = explosion.instance()
	explosion_instance.get_node("RegularPolygon").size = get_node("RegularPolygon").size
	explosion_instance.get_node("RegularPolygon/Polygon2D").set_color(get_node("RegularPolygon/Polygon2D").get_color())
	get_tree().get_root().add_child(explosion_instance)
	explosion_instance.set_global_pos(get_global_pos())


more swearing

more swearing

extends "res://scripts/entities/object.gd"

var camera
var hud
var newspaper

var player_id
var gamepad_id = null
var keyboard_use = null

var gamepad_handlers = []
var keyboard_handlers = []

var boctok_template = preload("res://scenes/ships/ship.tscn")
var mercury_template = preload("res://scenes/ships/mercury.tscn")

var rocket_template = preload("res://scenes/ships/rocket.tscn")
var rocket_cooldown = false
var swear_cooldown = false
var rocket_firing = false
var ROCKET_COOLDOWN_TIME = 0.1
var SWEAR_COOLDOWN_TIME = 2

var score = 0

func _init(bag, player_id).(bag):
    self.bag = bag
    self.player_id = player_id
    if player_id == 0:
        self.avatar = self.boctok_template.instance()
    else:
        self.avatar = self.mercury_template.instance()
    self.avatar.player_id = player_id
    self.camera = self.avatar.get_node('camera')

    self.gamepad_handlers = [
        preload("res://scripts/input/handlers/player_rotate_axis.gd").new(self.bag, self, 0),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 1),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 3),
        preload("res://scripts/input/handlers/player_accelerate_button.gd").new(self.bag, self, JOY_BUTTON_0),
        preload("res://scripts/input/handlers/player_boost_button.gd").new(self.bag, self, JOY_BUTTON_1),
        preload("res://scripts/input/handlers/rocket_launch_button.gd").new(self.bag, self, JOY_BUTTON_2),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 4),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 5),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 6),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 7),
        preload("res://scripts/input/handlers/swear_button.gd").new(self.bag, self, JOY_BUTTON_3),

    ]

    self.keyboard_handlers = [
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_LEFT, 1),
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_RIGHT, -1),
        preload("res://scripts/input/handlers/player_accelerate_key.gd").new(self.bag, self, KEY_UP),
        preload("res://scripts/input/handlers/player_boost_key.gd").new(self.bag, self, KEY_DOWN),
        preload("res://scripts/input/handlers/rocket_launch_key.gd").new(self.bag, self, KEY_SPACE),
        preload("res://scripts/input/handlers/swear_key.gd").new(self.bag, self, KEY_B),
    ]

func spawn(initial_position):
    self.attach()
    self.avatar.set_pos(initial_position)

func attach():
    self.bag.processing.register(self)
    self.newspaper.hide()
    self.hud.show()
    .attach()

func detach():
    self.bag.processing.remove(self)
    self.hud.hide()
    .detach()

func bind_keyboard():
    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = true
    for handler in self.keyboard_handlers:
        keyboard.register_handler(handler)
func unbind_keyboard():
    if not self.keyboard_use:
        return

    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = false
    for handler in self.keyboard_handlers:
        keyboard.remove_handler(handler)

func bind_gamepad(id):
    self.unbind_gamepad()
    self.gamepad_id = id
    var gamepad = self.bag.input.schemes['game']['pad' + str(id)]

    for handler in self.gamepad_handlers:
        gamepad.register_handler(handler)

func unbind_gamepad():
    if self.gamepad_id == null:
        return

    var gamepad = self.bag.input.schemes['game']['pad' + str(self.gamepad_id)]

    for handler in self.gamepad_handlers:
        gamepad.remove_handler(handler)

    self.gamepad_id = null

func reset():
    self.rocket_cooldown = false
    self.rocket_firing = false
    self.avatar.reset()

func bind_camera(viewport):
    self.camera.set_custom_viewport(viewport)

func bind_hud(hud):
    self.hud = hud

func bind_newspaper(newspaper):
    self.newspaper = newspaper

func show_fail():
    self.newspaper.show()
    if self.player_id == 0:
        self.newspaper.get_node('nyt').hide()
        self.newspaper.get_node('pravda').show()
    else:
        self.newspaper.get_node('nyt').show()
        self.newspaper.get_node('pravda').hide()

func process(delta):
    hud.update_sun_indicator(self.avatar.get_pos())
    hud.update_fuel(self.avatar.fuel)
    hud.update_gravity(self.avatar.current_gravity)
    hud.update_ship_velocity(self.avatar.current_acceleration)
    hud.update_score(self.score)

    if hud.update_sun_warning(self):
        self.swear()

    if self.rocket_firing and not self.rocket_cooldown:
        self.fire_rocket()

func fire_rocket():
    if self.rocket_cooldown:
        return

    var rocket = self.rocket_template.instance()
    var position = self.avatar.get_pos()
    var starting_vector = Vector2(0, -1)
    starting_vector.x = ((randi() % 101) - 50) / 900.0
    var rocket_offset = starting_vector.rotated(self.avatar.rotation)
    var rocket_position = position + rocket_offset * 60

    rocket.initial_velocity = self.avatar.current_acceleration + rocket_offset * 500

    self.bag.board.attach_object(rocket)
    rocket.set_pos(rocket_position)
    rocket.set_rot(rocket.initial_velocity.angle() + 3.14)

    self.rocket_cooldown = true
    self.bag.timers.set_timeout(self.ROCKET_COOLDOWN_TIME, self, "rocket_cooldown_done")

    rocket.owner = self.player_id
    self.bag.sound.play('rocket_launch')

func rocket_cooldown_done():
    self.rocket_cooldown = false

func swear_cooldown_done():
    self.swear_cooldown = false

func swear():
    if self.swear_cooldown:
        return

    randomize()
    var suffix
    var item
    if self.player_id == 0:
        suffix = "ru_"
    else:
        suffix = "en_"
    item = 'swear_' + suffix + str(randi() % 8 + 1)

    self.bag.sound.play(item)

    self.swear_cooldown = true
    self.bag.timers.set_timeout(self.SWEAR_COOLDOWN_TIME, self, "swear_cooldown_done")

extends "res://scripts/entities/object.gd"

var camera
var hud
var newspaper

var player_id
var gamepad_id = null
var keyboard_use = null

var gamepad_handlers = []
var keyboard_handlers = []

var boctok_template = preload("res://scenes/ships/ship.tscn")
var mercury_template = preload("res://scenes/ships/mercury.tscn")

var rocket_template = preload("res://scenes/ships/rocket.tscn")
var rocket_cooldown = false
var swear_cooldown = false
var rocket_firing = false
var ROCKET_COOLDOWN_TIME = 0.1
var SWEAR_COOLDOWN_TIME = 2

var score = 0

func _init(bag, player_id).(bag):
    self.bag = bag
    self.player_id = player_id
    if player_id == 0:
        self.avatar = self.boctok_template.instance()
    else:
        self.avatar = self.mercury_template.instance()
    self.avatar.player_id = player_id
    self.camera = self.avatar.get_node('camera')

    self.gamepad_handlers = [
        preload("res://scripts/input/handlers/player_rotate_axis.gd").new(self.bag, self, 0),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 1),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 3),
        preload("res://scripts/input/handlers/player_accelerate_button.gd").new(self.bag, self, JOY_BUTTON_0),
        preload("res://scripts/input/handlers/player_boost_button.gd").new(self.bag, self, JOY_BUTTON_1),
        preload("res://scripts/input/handlers/rocket_launch_button.gd").new(self.bag, self, JOY_BUTTON_2),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 4),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 5),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 6),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 7),
        preload("res://scripts/input/handlers/swear_button.gd").new(self.bag, self, JOY_BUTTON_3),

    ]

    self.keyboard_handlers = [
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_LEFT, 1),
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_RIGHT, -1),
        preload("res://scripts/input/handlers/player_accelerate_key.gd").new(self.bag, self, KEY_UP),
        preload("res://scripts/input/handlers/player_boost_key.gd").new(self.bag, self, KEY_DOWN),
        preload("res://scripts/input/handlers/rocket_launch_key.gd").new(self.bag, self, KEY_SPACE),
        preload("res://scripts/input/handlers/swear_key.gd").new(self.bag, self, KEY_B),
    ]

func spawn(initial_position):
    self.attach()
    self.avatar.set_pos(initial_position)

func attach():
    self.bag.processing.register(self)
    self.newspaper.hide()
    self.hud.show()
    .attach()

func detach():
    self.bag.processing.remove(self)
    self.hud.hide()
    .detach()

func bind_keyboard():
    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = true
    for handler in self.keyboard_handlers:
        keyboard.register_handler(handler)
func unbind_keyboard():
    if not self.keyboard_use:
        return

    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = false
    for handler in self.keyboard_handlers:
        keyboard.remove_handler(handler)

func bind_gamepad(id):
    self.unbind_gamepad()
    self.gamepad_id = id
    var gamepad = self.bag.input.schemes['game']['pad' + str(id)]

    for handler in self.gamepad_handlers:
        gamepad.register_handler(handler)

func unbind_gamepad():
    if self.gamepad_id == null:
        return

    var gamepad = self.bag.input.schemes['game']['pad' + str(self.gamepad_id)]

    for handler in self.gamepad_handlers:
        gamepad.remove_handler(handler)

    self.gamepad_id = null

func reset():
    self.rocket_cooldown = false
    self.rocket_firing = false
    self.avatar.reset()

func bind_camera(viewport):
    self.camera.set_custom_viewport(viewport)

func bind_hud(hud):
    self.hud = hud

func bind_newspaper(newspaper):
    self.newspaper = newspaper

func show_fail():
    self.newspaper.show()
    if self.player_id == 0:
        self.newspaper.get_node('nyt').hide()
        self.newspaper.get_node('pravda').show()
    else:
        self.newspaper.get_node('nyt').show()
        self.newspaper.get_node('pravda').hide()

func process(delta):
    hud.update_sun_indicator(self.avatar.get_pos())
    hud.update_fuel(self.avatar.fuel)
    hud.update_gravity(self.avatar.current_gravity)
    hud.update_ship_velocity(self.avatar.current_acceleration)
    hud.update_score(self.score)

    if hud.update_sun_warning(self):
        self.swear()

    if self.rocket_firing and not self.rocket_cooldown:
        self.fire_rocket()

func fire_rocket():
    if self.rocket_cooldown:
        return

    var rocket = self.rocket_template.instance()
    var position = self.avatar.get_pos()
    var starting_vector = Vector2(0, -1)
    starting_vector.x = ((randi() % 101) - 50) / 900.0
    var rocket_offset = starting_vector.rotated(self.avatar.rotation)
    var rocket_position = position + rocket_offset * 60

    rocket.initial_velocity = self.avatar.current_acceleration + rocket_offset * 500

    self.bag.board.attach_object(rocket)
    rocket.set_pos(rocket_position)
    rocket.set_rot(rocket.initial_velocity.angle() + 3.14)

    self.rocket_cooldown = true
    self.bag.timers.set_timeout(self.ROCKET_COOLDOWN_TIME, self, "rocket_cooldown_done")

    rocket.owner = self.player_id
    self.bag.sound.play('rocket_launch')

func rocket_cooldown_done():
    self.rocket_cooldown = false

func swear_cooldown_done():
    self.swear_cooldown = false

func swear():
    if self.swear_cooldown:
        return

    randomize()
    var suffix
    var item
    if self.player_id == 0:
        suffix = "ru_"
    else:
        suffix = "en_"
    item = 'swear_' + suffix + str(randi() % 8 + 1)
    if self.bag.sound.can_play_sound():
        self.bag.sound.play(item)

    self.swear_cooldown = true
    self.bag.timers.set_timeout(self.SWEAR_COOLDOWN_TIME, self, "swear_cooldown_done")


Some walls calibration

Some walls calibration

var bag

var segments = []
var last_visited_segment = -1

var SCREEN_WIDTH = Globals.get("display/width")
var SCREEN_HEIGHT = Globals.get("display/height")
var SCREEN_MARGIN = 200

var SEGMENT_SIZE = 60
var SEGMENT_LINE_HEIGHT = 36

var WALL_HEIGHT = 39
var WALL_HALF_WIDTH = 20

var platforms = {
    'grass' : [
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_big.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_medium.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_small.xscn') }
    ],
    'ice' : [
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_big.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_medium.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_small.xscn') }
    ]
}

var left_wall_template = preload('res://scenes/map/wall_left.xscn')
var right_wall_template = preload('res://scenes/map/wall_right.xscn')
var separator_template = preload('res://scenes/map/base_platform.xscn')
var playable_width = 0

func _init_bag(bag):
    self.bag = bag
    self.SCREEN_MARGIN = int((self.SCREEN_WIDTH - 900) / 2)
    self.playable_width = self.SCREEN_WIDTH - (self.SCREEN_MARGIN * 2)
    self.reset_map()

func reset_map():
    for segment in self.segments:
        self.destroy_unused_segment(segment)
    self.segments.clear()

func generate_next_map_segment():
    var next_segment_index = self.segments.size()

    var new_segment = []

    new_segment = self.add_walls(new_segment)
    new_segment = self.add_separator(new_segment)
    new_segment = self.add_platforms(new_segment, next_segment_index)

    self.segments.append(new_segment)
    self.apply_segment(next_segment_index)
    self.destroy_unused_segment_by_id(next_segment_index - 3)

func apply_segment(id):
    var segment_offset = id * self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    for object in self.segments[id]:
        self.bag.action_controller.attach_object(object.object)
        object.object.set_global_pos(Vector2(object.x, -object.y - segment_offset))

func destroy_unused_segment_by_id(id):
    if id < 0:
        return

    self.destroy_unused_segment(self.segments[id])

func destroy_unused_segment(segment):
    for object in segment:
        self.bag.action_controller.detach_object(object.object)
        object.object.queue_free()

    segment.clear()

func add_segment_object(segment, x, y, object):
    segment.append({
        'x' : x,
        'y' : y,
        'object' : object
    })

    return segment

func add_separator(segment):
    var separator_platform = self.separator_template.instance()
    var vertical_position = self.SCREEN_WIDTH / 2 - 640
    segment = self.add_segment_object(segment, vertical_position, self.SEGMENT_LINE_HEIGHT, separator_platform)
    return segment

func add_walls(segment):
    var height = 0
    var left_wall
    var right_wall
    var wall_offset
    var real_height

    while height < self.SEGMENT_SIZE:
        left_wall = self.left_wall_template.instance()
        right_wall = self.right_wall_template.instance()

        wall_offset = self.SCREEN_MARGIN + self.WALL_HALF_WIDTH
        real_height = (height + self.WALL_HEIGHT / 2) * self.SEGMENT_LINE_HEIGHT
        segment = self.add_segment_object(segment, wall_offset, real_height, left_wall)
        segment = self.add_segment_object(segment, wall_offset + self.playable_width + self.WALL_HALF_WIDTH * 2, real_height, right_wall)

        height = height + self.WALL_HEIGHT
    return segment

func update_segments(player_height):
    var segment_height = self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    var overflow = player_height % segment_height

    var segment_num = (player_height - overflow) / segment_height

    if overflow > 100 && segment_num > self.last_visited_segment:
        self.last_visited_segment = segment_num
        self.generate_next_map_segment()

func add_platforms(segment, index):
    var iterator = 2
    var last_iterator = 0

    var templates
    if index < 3:
        templates = self.platforms.grass
    else:
        templates = self.platforms.ice

    randomize()

    while iterator < self.SEGMENT_SIZE - 2:
        segment = self.generate_single_platform(segment, iterator, templates)

        last_iterator = iterator

        if randi() % 10 == 0:
            iterator = iterator + 5
        else:
            iterator = iterator + 4

    if last_iterator < self.SEGMENT_SIZE - 4:
        segment = self.generate_single_platform(segment, self.SEGMENT_SIZE - 2, templates)

    return segment

func generate_single_platform(segment, iterator, templates):
    var template = templates[randi() % 2]
    var new_platform = template.template.instance()

    var horizontal_position = randi() % (self.playable_width - template.width)
    horizontal_position = horizontal_position + self.SCREEN_MARGIN + int(template.width / 2)

    segment = self.add_segment_object(segment, horizontal_position, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)

    var offset = self.SCREEN_WIDTH / 2 - horizontal_position
    if abs(offset) > 100 && (randi() % 2 == 0):
        new_platform = templates[2].template.instance()
        horizontal_position = self.SCREEN_WIDTH / 2 + offset

        segment = self.add_segment_object(segment, horizontal_position, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)

    return segment

var bag

var segments = []
var last_visited_segment = -1

var SCREEN_WIDTH = Globals.get("display/width")
var SCREEN_HEIGHT = Globals.get("display/height")
var SCREEN_MARGIN = 200

var SEGMENT_SIZE = 60
var SEGMENT_LINE_HEIGHT = 36

var WALL_HEIGHT = 39
var WALL_HALF_WIDTH = 36

var platforms = {
    'grass' : [
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_big.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_medium.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_grass_small.xscn') }
    ],
    'ice' : [
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_big.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_medium.xscn') },
        { 'width' : 100, 'template' : preload('res://scenes/map/platform_ice_small.xscn') }
    ]
}

var left_wall_template = preload('res://scenes/map/wall_left.xscn')
var right_wall_template = preload('res://scenes/map/wall_right.xscn')
var separator_template = preload('res://scenes/map/base_platform.xscn')
var playable_width = 0

func _init_bag(bag):
    self.bag = bag
    self.SCREEN_MARGIN = int((self.SCREEN_WIDTH - 900) / 2)
    self.playable_width = self.SCREEN_WIDTH - (self.SCREEN_MARGIN * 2)
    self.reset_map()

func reset_map():
    for segment in self.segments:
        self.destroy_unused_segment(segment)
    self.segments.clear()

func generate_next_map_segment():
    var next_segment_index = self.segments.size()

    var new_segment = []

    new_segment = self.add_walls(new_segment)
    new_segment = self.add_separator(new_segment)
    new_segment = self.add_platforms(new_segment, next_segment_index)

    self.segments.append(new_segment)
    self.apply_segment(next_segment_index)
    self.destroy_unused_segment_by_id(next_segment_index - 3)

func apply_segment(id):
    var segment_offset = id * self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    for object in self.segments[id]:
        self.bag.action_controller.attach_object(object.object)
        object.object.set_global_pos(Vector2(object.x, -object.y - segment_offset))

func destroy_unused_segment_by_id(id):
    if id < 0:
        return

    self.destroy_unused_segment(self.segments[id])

func destroy_unused_segment(segment):
    for object in segment:
        self.bag.action_controller.detach_object(object.object)
        object.object.queue_free()

    segment.clear()

func add_segment_object(segment, x, y, object):
    segment.append({
        'x' : x,
        'y' : y,
        'object' : object
    })

    return segment

func add_separator(segment):
    var separator_platform = self.separator_template.instance()
    var vertical_position = self.SCREEN_WIDTH / 2 - 640
    segment = self.add_segment_object(segment, vertical_position, self.SEGMENT_LINE_HEIGHT, separator_platform)
    return segment

func add_walls(segment):
    var height = 0
    var left_wall
    var right_wall
    var wall_offset
    var real_height

    while height < self.SEGMENT_SIZE:
        left_wall = self.left_wall_template.instance()
        right_wall = self.right_wall_template.instance()

        wall_offset = self.SCREEN_MARGIN - self.WALL_HALF_WIDTH
        real_height = (height + self.WALL_HEIGHT / 2) * self.SEGMENT_LINE_HEIGHT
        segment = self.add_segment_object(segment, wall_offset, real_height, left_wall)
        segment = self.add_segment_object(segment, wall_offset + self.playable_width + self.WALL_HALF_WIDTH * 2, real_height, right_wall)

        height = height + self.WALL_HEIGHT
    return segment

func update_segments(player_height):
    var segment_height = self.SEGMENT_SIZE * self.SEGMENT_LINE_HEIGHT
    var overflow = player_height % segment_height

    var segment_num = (player_height - overflow) / segment_height

    if overflow > 100 && segment_num > self.last_visited_segment:
        self.last_visited_segment = segment_num
        self.generate_next_map_segment()

func add_platforms(segment, index):
    var iterator = 2
    var last_iterator = 0

    var templates
    if index < 3:
        templates = self.platforms.grass
    else:
        templates = self.platforms.ice

    randomize()

    while iterator < self.SEGMENT_SIZE - 2:
        segment = self.generate_single_platform(segment, iterator, templates)

        last_iterator = iterator

        if randi() % 10 == 0:
            iterator = iterator + 5
        else:
            iterator = iterator + 4

    if last_iterator < self.SEGMENT_SIZE - 4:
        segment = self.generate_single_platform(segment, self.SEGMENT_SIZE - 2, templates)

    return segment

func generate_single_platform(segment, iterator, templates):
    var template = templates[randi() % 2]
    var new_platform = template.template.instance()

    var horizontal_position = randi() % (self.playable_width - template.width)
    horizontal_position = horizontal_position + self.SCREEN_MARGIN + int(template.width / 2)

    segment = self.add_segment_object(segment, horizontal_position, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)

    var offset = self.SCREEN_WIDTH / 2 - horizontal_position
    if abs(offset) > 100 && (randi() % 2 == 0):
        new_platform = templates[2].template.instance()
        horizontal_position = self.SCREEN_WIDTH / 2 + offset

        segment = self.add_segment_object(segment, horizontal_position, (iterator + 1) * self.SEGMENT_LINE_HEIGHT, new_platform)

    return segment


Randomized shot deviation

Randomized shot deviation

extends "res://scripts/entities/object.gd"

var camera
var hud

var player_id
var gamepad_id = null
var keyboard_use = null

var gamepad_handlers = []
var keyboard_handlers = []

var boctok_template = preload("res://scenes/ships/ship.tscn")
var mercury_template = preload("res://scenes/ships/mercury.tscn")

var rocket_template = preload("res://scenes/ships/rocket.tscn")
var rocket_cooldown = false
var rocket_firing = false
var ROCKET_COOLDOWN_TIME = 0.1

func _init(bag, player_id).(bag):
    self.bag = bag
    self.player_id = player_id
    if player_id == 0:
        self.avatar = self.boctok_template.instance()
    else:
        self.avatar = self.mercury_template.instance()
    self.camera = self.avatar.get_node('camera')

    self.gamepad_handlers = [
        preload("res://scripts/input/handlers/player_rotate_axis.gd").new(self.bag, self, 0),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 1),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 3),
        preload("res://scripts/input/handlers/player_accelerate_button.gd").new(self.bag, self, JOY_BUTTON_0),
        preload("res://scripts/input/handlers/player_boost_button.gd").new(self.bag, self, JOY_BUTTON_1),
        preload("res://scripts/input/handlers/rocket_launch_button.gd").new(self.bag, self, JOY_BUTTON_2),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 4),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 5),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 6),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 7),
    ]

    self.keyboard_handlers = [
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_LEFT, 1),
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_RIGHT, -1),
        preload("res://scripts/input/handlers/player_accelerate_key.gd").new(self.bag, self, KEY_UP),
        preload("res://scripts/input/handlers/player_boost_key.gd").new(self.bag, self, KEY_DOWN),
        preload("res://scripts/input/handlers/rocket_launch_key.gd").new(self.bag, self, KEY_SPACE),
    ]

func spawn(initial_position):
    self.attach()
    self.avatar.set_pos(initial_position)

func attach():
    self.bag.processing.register(self)
    .attach()

func detach():
    self.bag.processing.remove(self)
    .detach()

func bind_keyboard():
    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = true
    for handler in self.keyboard_handlers:
        keyboard.register_handler(handler)
func unbind_keyboard():
    if not self.keyboard_use:
        return

    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = false
    for handler in self.keyboard_handlers:
        keyboard.remove_handler(handler)

func bind_gamepad(id):
    self.unbind_gamepad()
    self.gamepad_id = id
    var gamepad = self.bag.input.schemes['game']['pad' + str(id)]

    for handler in self.gamepad_handlers:
        gamepad.register_handler(handler)

func unbind_gamepad():
    if self.gamepad_id == null:
        return

    var gamepad = self.bag.input.schemes['game']['pad' + str(self.gamepad_id)]

    for handler in self.gamepad_handlers:
        gamepad.remove_handler(handler)

    self.gamepad_id = null

func reset():
    self.rocket_cooldown = false
    self.rocket_firing = false
    self.avatar.reset()

func bind_camera(viewport):
    self.camera.set_custom_viewport(viewport)

func bind_hud(hud):
    self.hud = hud

func process(delta):
    hud.update_sun_indicator(self.avatar.get_pos())
    hud.update_fuel(self.avatar.fuel)
    hud.update_gravity(self.avatar.current_gravity)
    hud.update_ship_velocity(self.avatar.current_acceleration)
    hud.update_sun_warning(self)

    if self.rocket_firing and not self.rocket_cooldown:
        self.fire_rocket()

func fire_rocket():
    if self.rocket_cooldown:
        return

    var rocket = self.rocket_template.instance()
    var position = self.avatar.get_pos()
    var starting_vector = Vector2(0, -1)
    starting_vector.x = ((randi() % 101) - 50) / 900.0
    print(starting_vector)
    var rocket_offset = starting_vector.rotated(self.avatar.rotation)
    var rocket_position = position + rocket_offset * 60

    rocket.initial_velocity = self.avatar.current_acceleration + rocket_offset * 500

    self.bag.board.attach_object(rocket)
    rocket.set_pos(rocket_position)
    rocket.set_rot(rocket.initial_velocity.angle() + 3.14)

    self.rocket_cooldown = true
    self.bag.timers.set_timeout(self.ROCKET_COOLDOWN_TIME, self, "rocket_cooldown_done")

func rocket_cooldown_done():
    self.rocket_cooldown = false

extends "res://scripts/entities/object.gd"

var camera
var hud

var player_id
var gamepad_id = null
var keyboard_use = null

var gamepad_handlers = []
var keyboard_handlers = []

var boctok_template = preload("res://scenes/ships/ship.tscn")
var mercury_template = preload("res://scenes/ships/mercury.tscn")

var rocket_template = preload("res://scenes/ships/rocket.tscn")
var rocket_cooldown = false
var rocket_firing = false
var ROCKET_COOLDOWN_TIME = 0.1

func _init(bag, player_id).(bag):
    self.bag = bag
    self.player_id = player_id
    if player_id == 0:
        self.avatar = self.boctok_template.instance()
    else:
        self.avatar = self.mercury_template.instance()
    self.camera = self.avatar.get_node('camera')

    self.gamepad_handlers = [
        preload("res://scripts/input/handlers/player_rotate_axis.gd").new(self.bag, self, 0),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 1),
        #preload("res://scripts/input/handlers/player_accelerate_axis.gd").new(self.bag, self, 3),
        preload("res://scripts/input/handlers/player_accelerate_button.gd").new(self.bag, self, JOY_BUTTON_0),
        preload("res://scripts/input/handlers/player_boost_button.gd").new(self.bag, self, JOY_BUTTON_1),
        preload("res://scripts/input/handlers/rocket_launch_button.gd").new(self.bag, self, JOY_BUTTON_2),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 4),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 5),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 6),
        preload("res://scripts/input/handlers/rocket_launch_trigger.gd").new(self.bag, self, 7),
    ]

    self.keyboard_handlers = [
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_LEFT, 1),
        preload("res://scripts/input/handlers/player_rotate_key.gd").new(self.bag, self, KEY_RIGHT, -1),
        preload("res://scripts/input/handlers/player_accelerate_key.gd").new(self.bag, self, KEY_UP),
        preload("res://scripts/input/handlers/player_boost_key.gd").new(self.bag, self, KEY_DOWN),
        preload("res://scripts/input/handlers/rocket_launch_key.gd").new(self.bag, self, KEY_SPACE),
    ]

func spawn(initial_position):
    self.attach()
    self.avatar.set_pos(initial_position)

func attach():
    self.bag.processing.register(self)
    .attach()

func detach():
    self.bag.processing.remove(self)
    .detach()

func bind_keyboard():
    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = true
    for handler in self.keyboard_handlers:
        keyboard.register_handler(handler)
func unbind_keyboard():
    if not self.keyboard_use:
        return

    var keyboard = self.bag.input.schemes['game']['keyboard']
    self.keyboard_use = false
    for handler in self.keyboard_handlers:
        keyboard.remove_handler(handler)

func bind_gamepad(id):
    self.unbind_gamepad()
    self.gamepad_id = id
    var gamepad = self.bag.input.schemes['game']['pad' + str(id)]

    for handler in self.gamepad_handlers:
        gamepad.register_handler(handler)

func unbind_gamepad():
    if self.gamepad_id == null:
        return

    var gamepad = self.bag.input.schemes['game']['pad' + str(self.gamepad_id)]

    for handler in self.gamepad_handlers:
        gamepad.remove_handler(handler)

    self.gamepad_id = null

func reset():
    self.rocket_cooldown = false
    self.rocket_firing = false
    self.avatar.reset()

func bind_camera(viewport):
    self.camera.set_custom_viewport(viewport)

func bind_hud(hud):
    self.hud = hud

func process(delta):
    hud.update_sun_indicator(self.avatar.get_pos())
    hud.update_fuel(self.avatar.fuel)
    hud.update_gravity(self.avatar.current_gravity)
    hud.update_ship_velocity(self.avatar.current_acceleration)
    hud.update_sun_warning(self)

    if self.rocket_firing and not self.rocket_cooldown:
        self.fire_rocket()

func fire_rocket():
    if self.rocket_cooldown:
        return

    var rocket = self.rocket_template.instance()
    var position = self.avatar.get_pos()
    var rocket_offset = Vector2(0, -1).rotated(self.avatar.rotation)
    var rocket_position = position + rocket_offset * 60

    rocket.initial_velocity = self.avatar.current_acceleration + rocket_offset * 500

    self.bag.board.attach_object(rocket)
    rocket.set_pos(rocket_position)
    rocket.set_rot(rocket.initial_velocity.angle() + 3.14)

    self.rocket_cooldown = true
    self.bag.timers.set_timeout(self.ROCKET_COOLDOWN_TIME, self, "rocket_cooldown_done")

func rocket_cooldown_done():
    self.rocket_cooldown = false


Smaller vibrations

Smaller vibrations

extends RigidBody2D

var bag
var ship = preload("res://scripts/ship.gd")

var GRAVITY_FACTOR = 100000000000

var current_acceleration = Vector2(0, 0)
var current_gravity = Vector2(0, 0)

var initial_velocity = Vector2(0, 0)
var started = false

var engines = []

var owner

func _integrate_forces(s):
    var pos = self.get_pos()

    if pos.distance_to(Vector2(5000, 5000)) > 3000:
        self.queue_free()

    var lv = s.get_linear_velocity()
    var step = s.get_step()

    var engines_on = {"Main" : true, "Left": false, "Right" : false}

    self.set_rot(lv.angle() + 3.14)

    self.current_gravity = s.get_total_gravity() * step * self.GRAVITY_FACTOR
    lv += self.current_gravity
    self.current_acceleration = lv

    if not self.started:
        lv += self.initial_velocity
        self.started = true

    s.set_linear_velocity(lv)
    self.__engines_start(engines_on)

func _colliding_body(body):
    if body extends self.ship and body.player_id != self.owner:
        self.bag.players.players[self.owner].score += 10

    if body extends self.ship:
        var device_id = self.bag.players.players[body.player_id].gamepad_id
        Input.start_joy_vibration(device_id, 0.5, 0, 0.2)
        self.bag.players.players[body.player_id].swear()

    self.bag.sound.play('rocket_bang')
    self.bag.board.add_explosion(self.get_pos())
    self.queue_free()

func __engines_start(engines):
    for engine in engines:
        if engines[engine] == true:
            self.engines[engine].set_emitting(true)

func _ready():
    self.bag = self.get_node("/root/boctok").bag
    set_fixed_process(true)
    var engines = self.get_node("Engines")
    self.engines = {
        "Main" : engines.get_node("Main"),
        "Left" : engines.get_node("Left"),
        "Right" : engines.get_node("Right"),
    }

extends RigidBody2D

var bag
var ship = preload("res://scripts/ship.gd")

var GRAVITY_FACTOR = 100000000000

var current_acceleration = Vector2(0, 0)
var current_gravity = Vector2(0, 0)

var initial_velocity = Vector2(0, 0)
var started = false

var engines = []

var owner

func _integrate_forces(s):
    var pos = self.get_pos()

    if pos.distance_to(Vector2(5000, 5000)) > 3000:
        self.queue_free()

    var lv = s.get_linear_velocity()
    var step = s.get_step()

    var engines_on = {"Main" : true, "Left": false, "Right" : false}

    self.set_rot(lv.angle() + 3.14)

    self.current_gravity = s.get_total_gravity() * step * self.GRAVITY_FACTOR
    lv += self.current_gravity
    self.current_acceleration = lv

    if not self.started:
        lv += self.initial_velocity
        self.started = true

    s.set_linear_velocity(lv)
    self.__engines_start(engines_on)

func _colliding_body(body):
    if body extends self.ship and body.player_id != self.owner:
        self.bag.players.players[self.owner].score += 10

    if body extends self.ship:
        var device_id = self.bag.players.players[body.player_id].gamepad_id
        Input.start_joy_vibration(device_id, 1, 0)
        self.bag.timers.set_timeout(0.2, Input, 'stop_joy_vibration', [device_id])
        self.bag.players.players[body.player_id].swear()

    self.bag.sound.play('rocket_bang')
    self.bag.board.add_explosion(self.get_pos())
    self.queue_free()

func __engines_start(engines):
    for engine in engines:
        if engines[engine] == true:
            self.engines[engine].set_emitting(true)

func _ready():
    self.bag = self.get_node("/root/boctok").bag
    set_fixed_process(true)
    var engines = self.get_node("Engines")
    self.engines = {
        "Main" : engines.get_node("Main"),
        "Left" : engines.get_node("Left"),
        "Right" : engines.get_node("Right"),
    }


Hide the player sprite if the game is over and show after the game is start

extends KinematicBody2D

const GRAVITY = 1000.0
const MOVE_SPEED = 260.0
const JUMP_FORCE = 230.0
const POINT = 1
const INIT_MOVE_DIRECTION = Vector2(1, 1)

var _velocity = Vector2()
var _motion = Vector2()
var is_grounded = false
var score = 0
var start_points = [
		Matrix32(0.0, Vector2(0, 80)),
		Matrix32(0.0, Vector2(715, 220)),
		Matrix32(0.0, Vector2(0, 360))
	]

var _move_direction = INIT_MOVE_DIRECTION
var current_floor = 1

onready var tree = get_tree()
onready var root = tree.get_root()
onready var _sprite = get_node("Sprite")
onready var _raycast = get_node("RayCast2D")
onready var _health = get_node("health")
onready var global = root.get_node("/root/global")

func _ready():
	set_process(true)
	set_fixed_process(true)

func _process(delta):
	if global.is_game_over():
		_sprite.hide()
	else:
		_sprite.show()

func _fixed_process(delta):
	is_grounded = _raycast.is_colliding()
	_velocity.y += _move_direction.y * GRAVITY * delta

	if not global.is_game_over():
		_velocity.x = _move_direction.x * MOVE_SPEED
	else:
		_velocity.x = 0.0

	if is_grounded:
		if Input.is_action_pressed("jump"):
			_velocity.y = -JUMP_FORCE

#	if OS.has_touchscreen_ui_hint():
#		pass
#		#Jump by touch screen
#	else:
#		pass
#		#Jump by keyboard or mouse

	_motion = _velocity * delta
	_motion = move(_motion)

	if is_colliding():
		var n = get_collision_normal()
		_motion = n.slide(_motion)
		_velocity = n.slide(_velocity)
		move(_motion)

func change_move_direction_h():
	_move_direction.x *= -1

func reset_move_direction():
	_move_direction = INIT_MOVE_DIRECTION

func _on_Area2D_area_enter( area ):
	if area.get_groups()[0] == "switch_side_trigger":
		global.add_score(POINT)

		if area.get_name() == "floor1-2":
			current_floor = 2
			set_transform(start_points[current_floor - 1])
			change_move_direction_h()
			_sprite.set_flip_h(true)

		elif area.get_name() == "floor2-3":
			current_floor = 3
			set_transform(start_points[current_floor - 1])
			change_move_direction_h()
			_sprite.set_flip_h(false)

		elif area.get_name() == "floor3-1":
			current_floor = 1
			set_transform(start_points[current_floor - 1])

func _on_Area2D_body_enter( body ):
	if body.get_groups()[0] == "totem":
		_health.remove(1)
		set_transform(start_points[current_floor - 1])

extends KinematicBody2D

const GRAVITY = 1000.0
const MOVE_SPEED = 260.0
const JUMP_FORCE = 230.0
const POINT = 1
const INIT_MOVE_DIRECTION = Vector2(1, 1)

var _velocity = Vector2()
var _motion = Vector2()
var is_grounded = false
var score = 0
var start_points = [
		Matrix32(0.0, Vector2(0, 80)),
		Matrix32(0.0, Vector2(715, 220)),
		Matrix32(0.0, Vector2(0, 360))
	]

var _move_direction = INIT_MOVE_DIRECTION
var current_floor = 1

onready var tree = get_tree()
onready var root = tree.get_root()
onready var _sprite = get_node("Sprite")
onready var _raycast = get_node("RayCast2D")
onready var _health = get_node("health")
onready var global = root.get_node("/root/global")

func _ready():
	set_fixed_process(true)

func _fixed_process(delta):
	is_grounded = _raycast.is_colliding()
	_velocity.y += _move_direction.y * GRAVITY * delta

	if not global.is_game_over():
		_velocity.x = _move_direction.x * MOVE_SPEED
	else:
		_velocity.x = 0.0

	if is_grounded:
		if Input.is_action_pressed("jump"):
			_velocity.y = -JUMP_FORCE

#	if OS.has_touchscreen_ui_hint():
#		pass
#		#Jump by touch screen
#	else:
#		pass
#		#Jump by keyboard or mouse

	_motion = _velocity * delta
	_motion = move(_motion)

	if is_colliding():
		var n = get_collision_normal()
		_motion = n.slide(_motion)
		_velocity = n.slide(_velocity)
		move(_motion)

func change_move_direction_h():
	_move_direction.x *= -1

func reset_move_direction():
	_move_direction = INIT_MOVE_DIRECTION

func _on_Area2D_area_enter( area ):
	if area.get_groups()[0] == "switch_side_trigger":
		global.add_score(POINT)

		if area.get_name() == "floor1-2":
			current_floor = 2
			set_transform(start_points[current_floor - 1])
			change_move_direction_h()
			_sprite.set_flip_h(true)

		elif area.get_name() == "floor2-3":
			current_floor = 3
			set_transform(start_points[current_floor - 1])
			change_move_direction_h()
			_sprite.set_flip_h(false)

		elif area.get_name() == "floor3-1":
			current_floor = 1
			set_transform(start_points[current_floor - 1])

func _on_Area2D_body_enter( body ):
	if body.get_groups()[0] == "totem":
		_health.remove(1)
		set_transform(start_points[current_floor - 1])


Testing

Testing

extends RigidBody2D

export var SPEEDUP = 4
const MAXSPEED = 800

var number_bricks = 0

func _ready():
	set_fixed_process(true);

func _fixed_process(delta):
	var bodies = get_colliding_bodies()

	for body in bodies:
		if body.is_in_group("Bricks"):
			get_node("/root/World").score += 420
			get_node("Horn").play("horn")
			body.queue_free()
			number_bricks += 1

		if body.get_name() == "Paddle":
			var speed = get_linear_velocity().length()
			var direction = get_pos() - body.get_node("Anchor").get_global_pos()
			var velocity = direction.normalized()*min(speed + SPEEDUP, MAXSPEED)
			set_linear_velocity(velocity)

		if(number_bricks == 16):
			get_tree().change_scene("res://Worlds/win_menu.tscn")

	if get_pos().y > get_viewport_rect().end.y:
		get_tree().change_scene("res://Worlds/death_menu.tscn")
		queue_free()

extends RigidBody2D

export var SPEEDUP = 4
const MAXSPEED = 800

var number_bricks = 0

func _ready():
	set_fixed_process(true);

func _fixed_process(delta):
	var bodies = get_colliding_bodies()

	for body in bodies:
		if body.is_in_group("Bricks"):
			get_node("/root/World").score += 5
			get_node("Horn").play("horn")
			body.queue_free()
			number_bricks += 1

		if body.get_name() == "Paddle":
			var speed = get_linear_velocity().length()
			var direction = get_pos() - body.get_node("Anchor").get_global_pos()
			var velocity = direction.normalized()*min(speed + SPEEDUP, MAXSPEED)
			set_linear_velocity(velocity)

		if(number_bricks == 16):
			get_tree().change_scene("res://Worlds/win_menu.tscn")

	if get_pos().y > get_viewport_rect().end.y:
		get_tree().change_scene("res://Worlds/death_menu.tscn")
		queue_free()


Dodge the Creeps: Set the physics parameters of the mob before adding it to the scene tree

This follows the recommendation from https://github.com/godotengine/godot/issues/45638

extends Node

export(PackedScene) var mob_scene
var score

func _ready():
	randomize()

func game_over():
	$ScoreTimer.stop()
	$MobTimer.stop()
	$HUD.show_game_over()
	$Music.stop()
	$DeathSound.play()

func new_game():
	get_tree().call_group("mobs", "queue_free")
	score = 0
	$Player.start($StartPosition.position)
	$StartTimer.start()
	$HUD.update_score(score)
	$HUD.show_message("Get Ready")
	$Music.play()

func _on_MobTimer_timeout():
	# Create a new instance of the Mob scene.
	var mob = mob_scene.instance()

	# Choose a random location on Path2D.
	var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
	mob_spawn_location.offset = randi()

	# Set the mob's direction perpendicular to the path direction.
	var direction = mob_spawn_location.rotation + PI / 2

	# Set the mob's position to a random location.
	mob.position = mob_spawn_location.position

	# Add some randomness to the direction.
	direction += rand_range(-PI / 4, PI / 4)
	mob.rotation = direction

	# Choose the velocity for the mob.
	var velocity = Vector2(rand_range(150.0, 250.0), 0.0)
	mob.linear_velocity = velocity.rotated(direction)

	# Spawn the mob by adding it to the Main scene.
	add_child(mob)

func _on_ScoreTimer_timeout():
	score += 1
	$HUD.update_score(score)

func _on_StartTimer_timeout():
	$MobTimer.start()
	$ScoreTimer.start()

extends Node

export(PackedScene) var mob_scene
var score

func _ready():
	randomize()

func game_over():
	$ScoreTimer.stop()
	$MobTimer.stop()
	$HUD.show_game_over()
	$Music.stop()
	$DeathSound.play()

func new_game():
	get_tree().call_group("mobs", "queue_free")
	score = 0
	$Player.start($StartPosition.position)
	$StartTimer.start()
	$HUD.update_score(score)
	$HUD.show_message("Get Ready")
	$Music.play()

func _on_MobTimer_timeout():
	# Choose a random location on Path2D.
	var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
	mob_spawn_location.offset = randi()

	# Create a Mob instance and add it to the scene.
	var mob = mob_scene.instance()
	add_child(mob)

	# Set the mob's direction perpendicular to the path direction.
	var direction = mob_spawn_location.rotation + PI / 2

	# Set the mob's position to a random location.
	mob.position = mob_spawn_location.position

	# Add some randomness to the direction.
	direction += rand_range(-PI / 4, PI / 4)
	mob.rotation = direction

	# Choose the velocity for the mob.
	var velocity = Vector2(rand_range(150.0, 250.0), 0.0)
	mob.linear_velocity = velocity.rotated(direction)

func _on_ScoreTimer_timeout():
	score += 1
	$HUD.update_score(score)

func _on_StartTimer_timeout():
	$MobTimer.start()
	$ScoreTimer.start()


load/save dialogs convey proper info to user

extends Spatial

var puzzleScn = preload("res://puzzle.scn")
var puzzle
var puzzleMan

var gridMan
var prevBlock = null

var fileDialog = load("res://fileDialog.scn").instance()
var gui = [
	["status", Label.new()], # plz keep me as first element, referenced as gui[0] later
	["save_pzl", Button.new()],
	["load_pzl", Button.new()],
	["remove_layer", Button.new()],
	["random_layer", Button.new()],
	# THESE SHOULD BE Options instead of left, label, right
	["class_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=preload("res://scripts/PuzzleManager.gd").blockColors,
		value="Red"
	}],
	["action_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]

func shouldAddNeighbor():
	return true

func shouldReplaceSelf():
	return false

func shouldRemoveSelf():
	return false

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
		gui[0][1].set_text("STATUS: ERROR, ADD PAIR")
	else:
		prevBlock = b
		gui[0][1].set_text("STATUS: NOMINAL")
	return b

func updateToggle(togg, increase):
	if increase:
		var next_ix = togg.values.find(togg.value) + 1
		if (next_ix >= togg.values.size()):
			next_ix = 0
		togg.value = togg.values[next_ix]
	else:
		var next_ix = togg.values.find(togg.value) - 1
		if (next_ix < 0):
			next_ix = togg.values.size() - 1
		togg.value =  togg.values[next_ix]
	togg.label.set_text(togg.value)

func showFileDialog(loadInstead=false):
	get_tree().set_pause(true)

	if loadInstead:
		fileDialog.connect("confirmed", self, "puzzleLoad")
		fileDialog.set_mode(FileDialog.MODE_OPEN_FILE)
	else:
		fileDialog.connect("confirmed", self, "puzzleSave")
		fileDialog.set_mode(FileDialog.MODE_SAVE_FILE)

	fileDialog.popup_centered()

func puzzleSave():
	print("SAVING TO ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func puzzleLoad():
	print("LOADING FROM ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func _ready():
	puzzle = puzzleScn.instance()
	gridMan = puzzle.get_node("GridView/GridMan")
	puzzle.mainPuzzle = false

	# hide and disable timer
	puzzle.time.on = false;
	puzzle.time.val = ''

	puzzle.set_as_toplevel(true)

	add_child(puzzle)

	print(puzzle.get_tree() == get_tree())
	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")

	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_USERDATA)
	fileDialog.set_current_dir("PuzzleSaves")
	fileDialog.add_filter("*.pzl ; Anttris Puzzle")

	# MainTheme leaks on bottom
	var dialogTheme = Theme.new()
	dialogTheme.copy_default_theme()
	fileDialog.set_theme(dialogTheme)

	# work even if game is paused
	fileDialog.set_pause_mode(PAUSE_MODE_PROCESS)

	# unpause if user cancels
	fileDialog.connect("popup_hide", get_tree(), "set_pause", [false])
	fileDialog.hide()
	add_child(fileDialog)

	var y = 0
	for control in gui:
		y += 45
		if control[0].rfind('_toggle') > 0:
			var togg = control[1]
			for cont in togg.keys():
				if not cont.begins_with('value'):
					var e = togg[cont]
					e.set_pos(Vector2(10, y))
					e.set_theme(theme)
					add_child(e)

			togg.left.set_text("<")
			togg.left.connect('pressed', self, 'updateToggle', [togg, false])

			togg.label.set_pos(Vector2(90, y + 4))
			togg.label.set_text(togg.value)

			togg.right.set_pos(Vector2(55, y))
			togg.right.set_text(">")
			togg.right.connect('pressed', self, 'updateToggle', [togg, true])

		else:
			var e = control[1]
			e.set_pos(Vector2(10, y))
			e.set_theme(theme)
			if e extends Button:
				e.set_text(control[0])
			if control[0] == 'save_pzl' :
				e.connect('pressed', self, 'showFileDialog')
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'showFileDialog', [true])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)

extends Spatial

var puzzleScn = preload("res://puzzle.scn")
var puzzle
var puzzleMan

var gridMan
var prevBlock = null

var fileDialog = load("res://fileDialog.scn").instance()
var gui = [
	["status", Label.new()], # plz keep me as first element, referenced as gui[0] later
	["save_pzl", Button.new()],
	["load_pzl", Button.new()],
	["remove_layer", Button.new()],
	["random_layer", Button.new()],
	# THESE SHOULD BE Options instead of left, label, right
	["class_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=preload("res://scripts/PuzzleManager.gd").blockColors,
		value="Red"
	}],
	["action_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]

func shouldAddNeighbor():
	return true

func shouldReplaceSelf():
	return false

func shouldRemoveSelf():
	return false

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
		gui[0][1].set_text("STATUS: ERROR, ADD PAIR")
	else:
		prevBlock = b
		gui[0][1].set_text("STATUS: NOMINAL")
	return b

func updateToggle(togg, increase):
	if increase:
		var next_ix = togg.values.find(togg.value) + 1
		if (next_ix >= togg.values.size()):
			next_ix = 0
		togg.value = togg.values[next_ix]
	else:
		var next_ix = togg.values.find(togg.value) - 1
		if (next_ix < 0):
			next_ix = togg.values.size() - 1
		togg.value =  togg.values[next_ix]
	togg.label.set_text(togg.value)

func showFileDialog(loadInstead=false):
	get_tree().set_pause(true)
	fileDialog.popup_centered()

	if loadInstead:
		fileDialog.connect("confirmed", self, "puzzleLoad")
	else:
		fileDialog.connect("confirmed", self, "puzzleSave")

func puzzleSave():
	print("SAVING TO ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func puzzleLoad():
	print("LOADING FROM ", fileDialog.get_current_file() )
	get_tree().set_pause(false)

func _ready():
	puzzle = puzzleScn.instance()
	gridMan = puzzle.get_node("GridView/GridMan")
	puzzle.mainPuzzle = false

	# hide and disable timer
	puzzle.time.on = false;
	puzzle.time.val = ''

	puzzle.set_as_toplevel(true)

	add_child(puzzle)

	print(puzzle.get_tree() == get_tree())
	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")

	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_USERDATA)

	# MainTheme leaks on bottom
	var dialogTheme = Theme.new()
	dialogTheme.copy_default_theme()
	fileDialog.set_theme(dialogTheme)

	# work even if game is paused
	fileDialog.set_pause_mode(PAUSE_MODE_PROCESS)

	# unpause if user cancels
	fileDialog.connect("popup_hide", get_tree(), "set_pause", [false])
	fileDialog.hide()
	add_child(fileDialog)

	var y = 0
	for control in gui:
		y += 45
		if control[0].rfind('_toggle') > 0:
			var togg = control[1]
			for cont in togg.keys():
				if not cont.begins_with('value'):
					var e = togg[cont]
					e.set_pos(Vector2(10, y))
					e.set_theme(theme)
					add_child(e)

			togg.left.set_text("<")
			togg.left.connect('pressed', self, 'updateToggle', [togg, false])

			togg.label.set_pos(Vector2(90, y + 4))
			togg.label.set_text(togg.value)

			togg.right.set_pos(Vector2(55, y))
			togg.right.set_text(">")
			togg.right.connect('pressed', self, 'updateToggle', [togg, true])

		else:
			var e = control[1]
			e.set_pos(Vector2(10, y))
			e.set_theme(theme)
			if e extends Button:
				e.set_text(control[0])
			if control[0] == 'save_pzl' :
				e.connect('pressed', self, 'showFileDialog')
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'showFileDialog', [true])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)


added basic GUI, replace and delete functions

extends Spatial

var puzzleMan
var gridMan
var prevBlock = null

var fileDialog = FileDialog.new()
var gui = [
	["status", Label.new()],
	["save_pzl", Button.new()],
	["load_pzl", Button.new()],
	["remove_layer", Button.new()],
	["random_layer", Button.new()],
	# THESE SHOULD BE Options instead of left, label, right
	["class_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=preload("res://scripts/PuzzleManager.gd").blockColors,
		value="Red"
	}],
	["action_toggle", {
		left=Button.new(),
		right=Button.new(),
		label=Label.new(),
		values=["Add", "Remove", Replace"],
		value="Add"
	}],
]

var puzzleScn = preload("res://puzzle.scn")

func shouldAddNeighbor():
	return false

func shouldReplaceSelf():
	return true

func shouldRemoveSelf():
	return true

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
	else:
		prevBlock = b
	return b

func updateToggle(togg, increase):
	print(togg.values[0], togg.values.size())
	if increase:
		var next_ix = togg.values.find(togg.value) + 1
		if (next_ix >= togg.values.size()):
			next_ix = 0
		togg.value = togg.values[next_ix]
	else:
		var next_ix = togg.values.find(togg.value) - 1
		if (next_ix < 0):
			next_ix = togg.values.size() - 1
		togg.value =  togg.values[next_ix]
	togg.label.set_text(togg.value)

func puzzleSave(loadInstead=false):
	fileDialog.popup_centered()

func _ready():
	var pMan = puzzleScn.instance()
	gridMan = pMan.get_node("GridView/GridMan")
	pMan.mainPuzzle = false
	pMan.time.on = false;
	pMan.time.val = ''
	pMan.set_as_toplevel(true)

	add_child(pMan)
	puzzleMan = pMan.puzzleMan

	set_process_input(true)

	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_USERDATA)
	fileDialog.set_theme(preload("res://themes/MainTheme.thm"))
	add_child(fileDialog)
	var y = 0
	for control in gui:
		y += 45
		if control[0].rfind('_toggle') > 0:
			var togg = control[1]
			for cont in togg.keys():
				if not cont.begins_with('value'):
					var e = togg[cont]
					e.set_pos(Vector2(10, y))
					e.set_theme(preload("res://themes/MainTheme.thm"))
					add_child(e)

			togg.left.set_text("<")
			togg.left.connect('pressed', self, 'updateToggle', [togg, false])

			togg.label.set_pos(Vector2(90, y + 4))
			togg.label.set_text(togg.value)

			togg.right.set_pos(Vector2(55, y))
			togg.right.set_text(">")
			togg.right.connect('pressed', self, 'updateToggle', [togg, true])

		else:
			var e = control[1]
			e.set_pos(Vector2(10, y))
			e.set_theme(load('res://themes/MainTheme.thm'))
			if e extends Button:
				e.set_text(control[0])
			if control[0] == 'save_pzl' :
				e.connect('pressed', self, 'puzzleSave')
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'puzzleSave', [true])
			add_child(e)
		if control[0] == 'status':
			control[1].set_text('STATUS: NOMINAL')

extends Spatial

var puzzleMan
var gridMan
var prevBlock = null

var puzzleScn = preload("res://puzzle.scn")

func shouldAddNeighbor():
	return not (gridMan.shape.keys().size() > 28)

func newPickledBlock():
	var b = puzzleMan.PickledBlock.new()\
		.setName(gridMan.shape.keys().size())\
	if prevBlock != null:
		b.setPairName(prevBlock.name)
		gridMan.get_node(prevBlock.toNode().name).setPairName(b.name)
		prevBlock = null
	else:
		prevBlock = b
	return b

func _ready():
	var pMan = puzzleScn.instance()
	gridMan = pMan.get_node("GridView/GridMan")
	pMan.mainPuzzle = false
	pMan.time.on = false
	pMan.set_as_toplevel(true)

	add_child(pMan)
	puzzleMan = pMan.puzzleMan

	set_process_input(true)


fixed bug in Android camera motion

fixed bug in Android camera motion

extends Spatial

# global working variables
var turn = Vector2( 0.0, 0.0 )            # the amount the mouse has turned
var mouseposlast = Input.get_mouse_pos()   # the mouses last position

# global tweakable parameters
var orbitrate = 10        # the rate the camera orbits the target when the mouse is moved

func _ready():
	# Initalization here
	set_process_input(true)      # process user input events here

# called to handle a user input event
func _input(ev):
	# If the mouse has been moved
	if (ev.type==InputEvent.SCREEN_DRAG or (ev.type==InputEvent.MOUSE_MOTION and ev.button_mask == 1)):
		# calculate the delta change from the last mouse movement
		var mousedelta = (mouseposlast - ev.pos)
		# scale the mouse delta to a useful value
		turn = mousedelta / orbitrate
		# perform the rotation
		var currentTransform = get_transform().basis
		var dtime = get_process_delta_time()
		currentTransform = Matrix3( Vector3(0, 1, 0), PI * turn.x * dtime ) * currentTransform
		currentTransform = Matrix3( Vector3(1, 0, 0), PI * turn.y * dtime ) * currentTransform
		set_transform( Transform( currentTransform ) )

	# record the last position of the mousedelta
	mouseposlast = ev.pos
	# Android does not like this: Input.get_mouse_pos()

extends Spatial

# global working variables
var turn = Vector2( 0.0, 0.0 )            # the amount the mouse has turned
var mouseposlast = Input.get_mouse_pos()   # the mouses last position

# global tweakable parameters
var orbitrate = 10        # the rate the camera orbits the target when the mouse is moved

func _ready():
	# Initalization here
	set_process_input(true)      # process user input events here

# called to handle a user input event
func _input(ev):
	# If the mouse has been moved
	if (ev.type==InputEvent.MOUSE_MOTION and ev.button_mask == 1):
		# calculate the delta change from the last mouse movement
		var mousedelta = (mouseposlast - ev.pos)
		# scale the mouse delta to a useful value
		turn = mousedelta / orbitrate
		# perform the rotation
		var currentTransform = get_transform().basis
		var dtime = get_process_delta_time()
		currentTransform = Matrix3( Vector3(0, 1, 0), PI * turn.x * dtime ) * currentTransform
		currentTransform = Matrix3( Vector3(1, 0, 0), PI * turn.y * dtime ) * currentTransform
		set_transform( Transform( currentTransform ) )

	# record the last position of the mousedelta
	mouseposlast = Input.get_mouse_pos()


Fix deep equality checking to use a "near" double comparison to allow for floating point errors.

# base test class

#extends Object

var _tests = []
var filename = "<unknown>"
var _results = null

func _init():
	var t
	for t in get_method_list():
		# print(t.to_json())
		if t["name"].begins_with("test_") && t["args"].size() <= 0:
			add(t["name"])

func class_setup():
	pass

func class_teardown():
	pass

func setup():
	pass

func teardown():
	pass

# ------------------------------------------------------------------------

func add(test_name):
	if test_name == null:
		return
	if typeof(test_name) == TYPE_STRING:
		if not(test_name in _tests):
			_tests.append(test_name)
	else:
		var t
		for t in test_name:
			if t != null && not(t in _tests):
				_tests.append(t)

func add_all(all_tests):
	add(all_tests)

func set_tests(test_names):
	_tests = []
	add(test_names)

func skip(test_name):
	if test_name == null:
		return
	if typeof(test_name) == TYPE_STRING:
		_tests.erase(test_name)
	else:
		var t
		for t in test_name:
			_tests.erase(test_name)

# ------------------------------------------------------------------------

func check_true(text, bool_val):
	if ! bool_val:
		if _results != null:
			_results.add_error(text)
		return false
	return true

func check_that(text, actual, matcher):
	return check_true(text + ": " + matcher.describe(actual), matcher.matches(actual))

func check(text = ""):
	return Checker.new(_results, text)

# ------------------------------------------------------------------------

# result_collector must implement these methods:
#   func start_suite(suite_name)
#   func end_suite()
#   func start_test(name)
#   func end_test()
#   func add_error(message)
#   func has_errors() (does the current suite have any errors?)

func run(result_collector):
	self._results = result_collector
	result_collector.start_suite(filename)
	class_setup()
	if result_collector.has_error():
		return

	var t
	for t in _tests:
		if has_method(t):
			run_test(t)
		else:
			result_collector.add_error("Setup Error: requested function does not exist: " + t)

	class_teardown()
	result_collector.end_suite()

func run_test(name):
	if _results != null:
		_results.start_test(name)
	setup()
	call(name)
	teardown()
	if _results != null:
		_results.end_test()

# -------------------------------------------------------------------------

class Matcher:
	func matches(value):
		return false
	func describe(value):
		return ""
	func _as_str(value):
		if value == null:
			return "<null>"
		if typeof(value) == TYPE_DICTIONARY:
			return value.to_json()
		return "[" + str(value) + "]"
	func _is_list(value):
		# return typeof(value) == TYPE_ARRAY || typeof(value) == TYPE_INT_ARRAY || typeof(value) == TYPE_REAL_ARRAY || typeof(value) == TYPE_STRING_ARRAY
		# All the arrays are in this specific range.
		# This needs to be checked against future versions of Godot.
		var v = typeof(value)
		return v >= TYPE_ARRAY && v <= TYPE_COLOR_ARRAY

class IsMatcher:
	extends Matcher
	var val

	func _init(v):
		val = v

	func matches(value):
		return _inner_match(val, value, [])

	func _inner_match(v1, v2, seen):
		seen.append(v2)
		# Check lists the same way
		if _is_list(v1) && _is_list(v2):
			if v1.size() != v2.size():
				return false
			var i
			for i in range(0, v1.size()):
				if v2[i] in seen:
					# This isn't a correct check; instead,
					# we need to ensure that the seen version of v2[i]
					# matches up with the corresponding seen version of v1[i]
					if v1[i] != v2[i]:
						return false
					# Else prevent infinite loop by just
					# saying it's right.
				elif ! _inner_match(v1[i], v2[i], seen):
					return false
			return true
		# Cannot perform a "==" if the types are different
		if typeof(v1) != typeof(v2):
			return false
		if v1 == v2:
			return true
		if typeof(v1) == TYPE_DICTIONARY:
			if v1.size() != v2.size():
				return false
			var k
			for k in v1:
				if not (k in v2):
					return false
				if v2[k] in seen:
					# This isn't a correct check; instead,
					# we need to ensure that the seen version of v2[i]
					# matches up with the corresponding seen version of v1[i]
					if v1[k] != v2[k]:
						return false
				elif ! _inner_match(v1[k], v2[k], seen):
					return false
			return true
		if typeof(v1) == TYPE_REAL:
			# Closeness
			return abs(float(v1) - v2) <= 0.0000001
		# Any other type should have == match right.
		return false

	func describe(value):
		return "expected " + _as_str(val) + ", found " + _as_str(value)

static func is(value):
	# "is" can be used to make a clear English-like sentence.
	# So, if the value is a matcher, then just use that matcher instead of
	# adding another layer around "is".
	if typeof(value) == TYPE_OBJECT && value.has_method("matches") && value.has_method("describe"):
		return value
	# Need to wrap the value in an is check.
	return IsMatcher.new(value)

static func equals(value):
	return IsMatcher.new(value)

class NotMatcher:
	var val

	func _init(v):
		if typeof(v) == TYPE_OBJECT && v extends Matcher:
			val = v
		else:
			val = IsMatcher.new(v)

	func matches(value):
		return ! val.matches(value)

	func describe(value):
		return "expected not: " + val.describe(value)

static func is_not(value):
	return NotMatcher.new(value)

class BetweenMatcher:
	var lo
	var hi

	func _init(l, h):
		lo = float(l)
		hi = float(h)

	func matches(value):
		return value != null && float(value) >= lo && float(value) <= hi

	func describe(value):
		return "expected [" + str(lo) + ", " + str(hi) + "], found " + str(value)

static func between(lo, hi):
	return BetweenMatcher.new(lo, hi)

class NearMatcher:
	var _epsilon
	var _val

	func _init(val, epsilon = 0.00001):
		_epsilon = float(epsilon)
		_val = float(val)

	func matches(value):
		return abs(float(value) - _val) <= _epsilon

	func describe(value):
		return "expected " + str(_val) + " within " + str(_epsilon) + ", found " + str(value)

static func near(val, epsilon = 0.00001):
	return NearMatcher.new(val, epsilon)

class ContainsMatcher:
	extends Matcher
	var _val

	func _init(val):
		_val = val

	func matches(actual):
		if actual == null:
			return false
		if typeof(actual) == TYPE_STRING:
			# Expect a string to contain a sub-string
			return actual.find(_val) >= 0
		if _is_list(actual):
			if _is_list(_val):
				# each "val" must be in the actual list
				var v
				for v in _val:
					if ! (v in actual):
						return false
				return true
			return _val in actual
		if typeof(actual) == TYPE_RECT2:
			if typeof(_val) == TYPE_RECT2:
				return actual.encloses(_val)
			if typeof(_val) == TYPE_VECTOR2:
				return actual.has_point(_val)
			return false
		if typeof(actual) == TYPE_PLANE:
			if typeof(_val) == TYPE_VECTOR3:
				return actual.has_point(_val)
			return false
		if typeof(actual) == TYPE_DICTIONARY:
			if _is_list(_val):
				return actual.has_all(_val)
		 	return actual.has(_val)

		# These don't really make sense.

		# Object values should just be checked for equality.
		#if typeof(actual) == TYPE_OBJECT:
		#	return actual.get(_val) != null

		return false

	func describe(actual):
		return "expected " + _as_str(actual) + " to contain " + _as_str(_val)

static func contains(val):
	return ContainsMatcher.new(val)

class EmptyMatcher:
	extends Matcher

	func matches(actual):
		if _is_list(actual):
			return actual.size() <= 0
		if typeof(actual) == TYPE_DICTIONARY:
			return actual.keys().size() <= 0
		return false

	func describe(actual):
		if _is_list(actual):
			return "expected " + _as_str(actual) + " to be an empty list"
		if typeof(actual) == TYPE_DICTIONARY:
			return "expected " + _as_str(actual) + " to be an empty dictionary"
		return "expected " + _as_str(actual) + " to be an empty list or dictionary"

static func empty():
	return EmptyMatcher.new()

# ---------------------------------------------------------------------------

class Checker:
	var _text
	var _results

	func _init(results, text):
		_results = results
		_text = text

	func that(actual, matcher):
		var res
		var msg
		if typeof(matcher) == TYPE_BOOL:
			res = matcher
			msg = _text
		else:
			res = matcher.matches(actual)
			msg = _text + ": " + matcher.describe(actual)
		if ! res:
			if _results != null:
				_results.add_error(msg)
			return false
		return true

# base test class

#extends Object

var _tests = []
var filename = "<unknown>"
var _results = null

func _init():
	var t
	for t in get_method_list():
		# print(t.to_json())
		if t["name"].begins_with("test_") && t["args"].size() <= 0:
			add(t["name"])

func class_setup():
	pass

func class_teardown():
	pass

func setup():
	pass

func teardown():
	pass

# ------------------------------------------------------------------------

func add(test_name):
	if test_name == null:
		return
	if typeof(test_name) == TYPE_STRING:
		if not(test_name in _tests):
			_tests.append(test_name)
	else:
		var t
		for t in test_name:
			if t != null && not(t in _tests):
				_tests.append(t)

func add_all(all_tests):
	add(all_tests)

func set_tests(test_names):
	_tests = []
	add(test_names)

func skip(test_name):
	if test_name == null:
		return
	if typeof(test_name) == TYPE_STRING:
		_tests.erase(test_name)
	else:
		var t
		for t in test_name:
			_tests.erase(test_name)

# ------------------------------------------------------------------------

func check_true(text, bool_val):
	if ! bool_val:
		if _results != null:
			_results.add_error(text)
		return false
	return true

func check_that(text, actual, matcher):
	return check_true(text + ": " + matcher.describe(actual), matcher.matches(actual))

func check(text = ""):
	return Checker.new(_results, text)

# ------------------------------------------------------------------------

# result_collector must implement these methods:
#   func start_suite(suite_name)
#   func end_suite()
#   func start_test(name)
#   func end_test()
#   func add_error(message)
#   func has_errors() (does the current suite have any errors?)

func run(result_collector):
	self._results = result_collector
	result_collector.start_suite(filename)
	class_setup()
	if result_collector.has_error():
		return

	var t
	for t in _tests:
		if has_method(t):
			run_test(t)
		else:
			result_collector.add_error("Setup Error: requested function does not exist: " + t)

	class_teardown()
	result_collector.end_suite()

func run_test(name):
	if _results != null:
		_results.start_test(name)
	setup()
	call(name)
	teardown()
	if _results != null:
		_results.end_test()

# -------------------------------------------------------------------------

class Matcher:
	func matches(value):
		return false
	func describe(value):
		return ""
	func _as_str(value):
		if value == null:
			return "<null>"
		if typeof(value) == TYPE_DICTIONARY:
			return value.to_json()
		return "[" + str(value) + "]"
	func _is_list(value):
		# return typeof(value) == TYPE_ARRAY || typeof(value) == TYPE_INT_ARRAY || typeof(value) == TYPE_REAL_ARRAY || typeof(value) == TYPE_STRING_ARRAY
		# All the arrays are in this specific range.
		# This needs to be checked against future versions of Godot.
		var v = typeof(value)
		return v >= TYPE_ARRAY && v <= TYPE_COLOR_ARRAY

class IsMatcher:
	extends Matcher
	var val

	func _init(v):
		val = v

	func matches(value):
		return _inner_match(val, value, [])

	func _inner_match(v1, v2, seen):
		seen.append(v2)
		# Check lists the same way
		if _is_list(v1) && _is_list(v2):
			if v1.size() != v2.size():
				return false
			var i
			for i in range(0, v1.size()):
				if v2[i] in seen:
					# This isn't a correct check; instead,
					# we need to ensure that the seen version of v2[i]
					# matches up with the corresponding seen version of v1[i]
					if v1[i] != v2[i]:
						return false
					# Else prevent infinite loop by just
					# saying it's right.
				elif ! _inner_match(v1[i], v2[i], seen):
					return false
			return true
		# Cannot perform a "==" if the types are different
		if typeof(v1) != typeof(v2):
			return false
		if v1 == v2:
			return true
		if typeof(v1) == TYPE_DICTIONARY:
			if v1.size() != v2.size():
				return false
			var k
			for k in v1:
				if not (k in v2):
					return false
				if v2[k] in seen:
					# This isn't a correct check; instead,
					# we need to ensure that the seen version of v2[i]
					# matches up with the corresponding seen version of v1[i]
					if v1[k] != v2[k]:
						return false
				elif ! _inner_match(v1[k], v2[k], seen):
					return false
			return true
		# Any other type should have == match right.
		return false

	func describe(value):
		return "expected " + _as_str(val) + ", found " + _as_str(value)

static func is(value):
	# "is" can be used to make a clear English-like sentence.
	# So, if the value is a matcher, then just use that matcher instead of
	# adding another layer around "is".
	if typeof(value) == TYPE_OBJECT && value.has_method("matches") && value.has_method("describe"):
		return value
	# Need to wrap the value in an is check.
	return IsMatcher.new(value)

static func equals(value):
	return IsMatcher.new(value)

class NotMatcher:
	var val

	func _init(v):
		if typeof(v) == TYPE_OBJECT && v extends Matcher:
			val = v
		else:
			val = IsMatcher.new(v)

	func matches(value):
		return ! val.matches(value)

	func describe(value):
		return "expected not: " + val.describe(value)

static func is_not(value):
	return NotMatcher.new(value)

class BetweenMatcher:
	var lo
	var hi

	func _init(l, h):
		lo = float(l)
		hi = float(h)

	func matches(value):
		return value != null && float(value) >= lo && float(value) <= hi

	func describe(value):
		return "expected [" + str(lo) + ", " + str(hi) + "], found " + str(value)

static func between(lo, hi):
	return BetweenMatcher.new(lo, hi)

class NearMatcher:
	var _epsilon
	var _val

	func _init(val, epsilon = 0.00001):
		_epsilon = float(epsilon)
		_val = float(val)

	func matches(value):
		return abs(float(value) - _val) <= _epsilon

	func describe(value):
		return "expected " + str(_val) + " within " + str(_epsilon) + ", found " + str(value)

static func near(val, epsilon = 0.00001):
	return NearMatcher.new(val, epsilon)

class ContainsMatcher:
	extends Matcher
	var _val

	func _init(val):
		_val = val

	func matches(actual):
		if actual == null:
			return false
		if typeof(actual) == TYPE_STRING:
			# Expect a string to contain a sub-string
			return actual.find(_val) >= 0
		if _is_list(actual):
			if _is_list(_val):
				# each "val" must be in the actual list
				var v
				for v in _val:
					if ! (v in actual):
						return false
				return true
			return _val in actual
		if typeof(actual) == TYPE_RECT2:
			if typeof(_val) == TYPE_RECT2:
				return actual.encloses(_val)
			if typeof(_val) == TYPE_VECTOR2:
				return actual.has_point(_val)
			return false
		if typeof(actual) == TYPE_PLANE:
			if typeof(_val) == TYPE_VECTOR3:
				return actual.has_point(_val)
			return false
		if typeof(actual) == TYPE_DICTIONARY:
			if _is_list(_val):
				return actual.has_all(_val)
		 	return actual.has(_val)

		# These don't really make sense.

		# Object values should just be checked for equality.
		#if typeof(actual) == TYPE_OBJECT:
		#	return actual.get(_val) != null

		return false

	func describe(actual):
		return "expected " + _as_str(actual) + " to contain " + _as_str(_val)

static func contains(val):
	return ContainsMatcher.new(val)

class EmptyMatcher:
	extends Matcher

	func matches(actual):
		if _is_list(actual):
			return actual.size() <= 0
		if typeof(actual) == TYPE_DICTIONARY:
			return actual.keys().size() <= 0
		return false

	func describe(actual):
		if _is_list(actual):
			return "expected " + _as_str(actual) + " to be an empty list"
		if typeof(actual) == TYPE_DICTIONARY:
			return "expected " + _as_str(actual) + " to be an empty dictionary"
		return "expected " + _as_str(actual) + " to be an empty list or dictionary"

static func empty():
	return EmptyMatcher.new()

# ---------------------------------------------------------------------------

class Checker:
	var _text
	var _results

	func _init(results, text):
		_results = results
		_text = text

	func that(actual, matcher):
		var res
		var msg
		if typeof(matcher) == TYPE_BOOL:
			res = matcher
			msg = _text
		else:
			res = matcher.matches(actual)
			msg = _text + ": " + matcher.describe(actual)
		if ! res:
			if _results != null:
				_results.add_error(msg)
			return false
		return true