Remove debug print

Remove debug print

# The MIT License (MIT)
#
# Copyright (c) 2016 George Marques
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

extends TextureFrame

func _ready():
	get_node("Buttons/NewGameButton").call_deferred("grab_focus")
	set_process_unhandled_input(true)

func _unhandled_input(event):
	if event.is_pressed() and not event.is_echo() and event.is_action("ui_cancel"):
		accept_event()
		_on_ExitButton_pressed()

func _on_button_focused(description):
	get_node("DescriptionText").set_text(description)

# Button routines

func _on_NewGameButton_pressed():
	var loading_screen = load("res://menus/loading_screen.tscn").instance()
	add_child(loading_screen)
	loading_screen.connect("exit_tree", get_node("Buttons/NewGameButton"), "grab_focus")

func _on_SaveGameButton_pressed():
	var save_screen = load("res://menus/save_load_screen.tscn").instance()
	add_child(save_screen)
	save_screen.get_node("TitlePlank/TitleText").set_text("Save")
	save_screen.connect("exit_tree", get_node("Buttons/SaveGameButton"), "grab_focus")

func _on_LoadGameButton_pressed():
	var load_screen = load("res://menus/save_load_screen.tscn").instance()
	add_child(load_screen)
	load_screen.connect("exit_tree", get_node("Buttons/LoadGameButton"), "grab_focus")

func _on_ControlsButton_pressed():
	var controls_screen = load("res://menus/controls_screen.tscn").instance()
	add_child(controls_screen)
	controls_screen.connect("exit_tree", get_node("Buttons/ControlsButton"), "grab_focus")

func _on_HelpButton_pressed():
	var help_screen = load("res://menus/help_screen.tscn").instance()
	add_child(help_screen)
	help_screen.connect("exit_tree", get_node("Buttons/HelpButton"), "grab_focus")

func _on_ExitButton_pressed():
	var message_box = get_node("MessageBox")
	message_box.popup()
	message_box.grab_focus()

# Message Box

func _on_MessageBox_canceled():
	get_node("MessageBox").hide()

func _on_MessageBox_confirmed():
	get_tree().quit()

# The MIT License (MIT)
#
# Copyright (c) 2016 George Marques
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

extends TextureFrame

func _ready():
	get_node("Buttons/NewGameButton").call_deferred("grab_focus")
	set_process_unhandled_input(true)

func _unhandled_input(event):
	if event.is_pressed() and not event.is_echo() and event.is_action("ui_cancel"):
		accept_event()
		print("evet")
		_on_ExitButton_pressed()

func _on_button_focused(description):
	get_node("DescriptionText").set_text(description)

# Button routines

func _on_NewGameButton_pressed():
	var loading_screen = load("res://menus/loading_screen.tscn").instance()
	add_child(loading_screen)
	loading_screen.connect("exit_tree", get_node("Buttons/NewGameButton"), "grab_focus")

func _on_SaveGameButton_pressed():
	var save_screen = load("res://menus/save_load_screen.tscn").instance()
	add_child(save_screen)
	save_screen.get_node("TitlePlank/TitleText").set_text("Save")
	save_screen.connect("exit_tree", get_node("Buttons/SaveGameButton"), "grab_focus")

func _on_LoadGameButton_pressed():
	var load_screen = load("res://menus/save_load_screen.tscn").instance()
	add_child(load_screen)
	load_screen.connect("exit_tree", get_node("Buttons/LoadGameButton"), "grab_focus")

func _on_ControlsButton_pressed():
	var controls_screen = load("res://menus/controls_screen.tscn").instance()
	add_child(controls_screen)
	controls_screen.connect("exit_tree", get_node("Buttons/ControlsButton"), "grab_focus")

func _on_HelpButton_pressed():
	var help_screen = load("res://menus/help_screen.tscn").instance()
	add_child(help_screen)
	help_screen.connect("exit_tree", get_node("Buttons/HelpButton"), "grab_focus")

func _on_ExitButton_pressed():
	var message_box = get_node("MessageBox")
	message_box.popup()
	message_box.grab_focus()

# Message Box

func _on_MessageBox_canceled():
	get_node("MessageBox").hide()

func _on_MessageBox_confirmed():
	get_tree().quit()


moved camera setup to function. camera setup more robust. use puzzle.shape instead of copying

extends Spatial

# Is there a better way to do this?
const BLOCK_LASER	= 0
const BLOCK_WILD	= 1
const BLOCK_PAIR	= 2
const BLOCK_GOAL	= 3
const BLOCK_BLOCK   = 4

# Shape dictionary to access blocks quickly by position.
var puzzle.shape = {}

# Block selection handling.
var offClick = false
var selectedBlocks = []

# Puzzle vars.
var puzzle
var puzzleLoaded = false

# Beam stuff.
const beamScn = preload( "res://blocks/block.scn" )
const Beam = preload("res://scripts/Blocks/Beam.gd")

# sounds
var samplePlayer = SamplePlayer.new()

func addPickledBlock(block):
	var b = block.toNode()

	puzzle.shape[b.blockPos] = b

	# TODO  any special treatment for the wild blocks?
	# TODO  keep track of puzzle.lasers ? How to?

	if puzzleLoaded:
		# keep pickled block
		puzzle.blocks.append(block)

		# keep track of puzzle.pairCounts
		var layer = calcBlockLayerVec(b.blockPos)
		if b.getBlockType() == 2:
			while puzzle.pairCount.size() <= layer:
				puzzle.pairCount.append(0)
			puzzle.pairCount[layer] += 0.5

	add_child(b)
	return b

func get_block(pos):
	if puzzle.shape.has(pos):
		return puzzle.shape[pos]
	else:
		return null

# unused key argument needed for the tween_complete signal
func remove_block(block_node, key=null):
	if block_node == null:
		return

	# need better way
	for i in range(puzzle.blocks.size()):
		if block_node.blockPos == puzzle.blocks[i].blockPos:
			puzzle.blocks.remove(i)

	puzzle.shape[block_node.blockPos] = null
	for child in block_node.get_children():
		block_node.remove_and_delete_child(child)
	remove_and_delete_child(block_node)

# Sets the puzzle for this GridMan.
func set_puzzle(puzz):
	# verify puzzle
	if puzz == null:
		print("INVALID PUZZLE")
		return

	# delete all current nodes
	for pos in puzzle.shape:
		remove_block(puzzle.shape[pos])
	puzzleLoaded = false

	# Store the puzzle.
	puzzle = puzz

		# # I can do my own counting!
	# needed for adding blocks in the editor
	for block in puzzle.blocks:
		# Create a block node, add it to the tree
		addPickledBlock(block)
	puzzleLoaded = true

# Clears any selected blocks. WE SHOULD FIX THIS, THERE CAN ONLY BE ONE BLOCK SELECTED AT ANY ONE TIME, NO NEED FOR AN ARRAY!
func clearSelection():
	for bl in selectedBlocks:
		var blo = get_node( bl )
		blo.setSelected(false)
		blo.scaleTweenNode(1.0).start()
	selectedBlocks = []

# Add the selected block to the selected list. WE SHOULD FIX THIS, IT ONLY NEEDS TO STORE IT, NOT AN ARRAY!
func addSelected(bl):
	selectedBlocks.append(bl)

# Used for the multiplayer mode to force click a block on their side.
func forceClickBlock( pos ):
	puzzle.shape[pos].forceClick()

func clickBlock( name ):
	#now check if that was the second block we picked. If it was, we want to
	#unselect the blocks again
	if (offClick):
		offClick = false
		addSelected(name)
		clearSelection()
	else:
		offClick = true;
		addSelected(name)

# Calculates the layer that a block is on.
# COPY OF FUNCTION IN PUZZLEMAN, IS THERE A BETTER WAY TO DO THIS?!
func calcBlockLayer( x, y, z ):
	return max( max( abs( x ), abs( y ) ), abs( z ) )
func calcBlockLayerVec( pos ):
	return max( max( abs( pos.x ), abs( pos.y ) ), abs( pos.z ) )

# Handles keeping track of pairs being removed.
func popPair( pos ):
	var blayer = calcBlockLayerVec( pos )
	puzzle.pairCount[blayer] -= 1

	if( puzzle.pairCount[blayer] == 0 ):
		print("LAYER CLEARED")
		if blayer == 1:
			print( "GAME OVER!" )
			var pauseMenu = get_tree().get_root().get_node( "Spatial" ).get_node( "Camera" ).pauseMenu
			pauseMenu.set_text("GAME OVER\nSCORE:1,000,000")
			# TODO set timeout!!!
			pauseMenu.popup_centered()

		for b in puzzle.shape:
			if not ( puzzle.shape[b] == null ):
				if calcBlockLayerVec( b ) == blayer:
					if puzzle.shape[b].getBlockType() == BLOCK_LASER:
						puzzle.shape[b].forceActivate()

		# Fire beams.
		var beamNum = 0
		for l in range( puzzle.lasers.size() ):
			if calcBlockLayerVec( puzzle.lasers[l][0] ) == blayer:
				# Firin mah lazerz!
				var beam = beamScn.instance()

				beam.set_name( str(blayer) + "_beam_" + str(beamNum) )
				beamNum += 1
				beam.set_script( Beam )

				add_child( beam )
				beam.fire( puzzle.lasers[l][0], puzzle.lasers[l][1] )

				samplePlayer.play( "soundslikewillem_hitting_slinky" )

func _init():
	# Load sound
	samplePlayer.set_voice_count(10)
	samplePlayer.set_sample_library(ResourceLoader.load("new_samplelibrary.xml"))
	print("GridMan initialized")

func _ready():
	setupCam()

func setupCam():
	var cam = get_tree().get_root().get_node( "Spatial/Camera" )
	if cam == null or puzzle == null:
		return
	var totalSize = ( puzzle.puzzleLayers * 2 + 1 )
	cam.distance.val = 4.5 * totalSize
	cam.distance.min_ = 3 * totalSize
	cam.distance.max_ = 10 * totalSize
	cam.recalculate_camera()

extends Spatial

# Is there a better way to do this?
const BLOCK_LASER	= 0
const BLOCK_WILD	= 1
const BLOCK_PAIR	= 2
const BLOCK_GOAL	= 3
const BLOCK_BLOCK   = 4

# Shape dictionary to access blocks quickly by position.
var shape = {}

# Block selection handling.
var offClick = false
var selectedBlocks = []

# Puzzle vars.
var puzzle
var puzzleLoaded = false

# Beam stuff.
const beamScn = preload( "res://blocks/block.scn" )
const Beam = preload("res://scripts/Blocks/Beam.gd")

# sounds
var samplePlayer = SamplePlayer.new()

func addPickledBlock(block):
	# TODO add to puzzle
	var b = block.toNode()

	print ("ADDED")
	shape[b.blockPos] = b

	# TODO  any special treatment for the wild blocks?
	# TODO  keep track of puzzle.lasers ? How to?

	if puzzleLoaded:
		# keep pickled block
		puzzle.blocks.append(block)

		# keep track of puzzle.pairCounts
		var layer = calcBlockLayerVec(b.blockPos)
		if b.getBlockType() == 2:
			while puzzle.pairCount.size() <= layer:
				puzzle.pairCount.append(0)
			puzzle.pairCount[layer] += 0.5

	add_child(b)
	return b

func get_block(pos):
	if shape.has(pos):
		return shape[pos]
	else:
		return null

# unused key argument needed for the tween_complete signal
func remove_block(block_node, key=null):
	if block_node == null:
		return
	print ("REMOVED")

	shape[block_node.blockPos] = null
	for child in block_node.get_children():
		block_node.remove_and_delete_child(child)
	remove_and_delete_child(block_node)

# Sets the puzzle for this GridMan.
func set_puzzle(puzz):
	# delete all current nodes
	for pos in shape:
		remove_block(shape[pos])
	puzzleLoaded = false

	# Store the puzzle.
	puzzle = puzz

	# Set the camera range to be relative to the layer count.
	var cam = get_tree().get_root().get_node( "Spatial" ).get_node( "Camera" )
	print( cam )
	var totalSize = ( puzzle.puzzleLayers * 2 + 1 )
	cam.distance.val = 4.5 * totalSize
	cam.distance.min_ = 3 * totalSize
	cam.distance.max_ = 10 * totalSize
	cam.recalculate_camera()

		# # I can do my own counting!
	# needed for adding blocks in the editor
	for block in puzzle.blocks:
		# Create a block node, add it to the tree
		addPickledBlock(block)
	puzzleLoaded = true

# Clears any selected blocks. WE SHOULD FIX THIS, THERE CAN ONLY BE ONE BLOCK SELECTED AT ANY ONE TIME, NO NEED FOR AN ARRAY!
func clearSelection():
	for bl in selectedBlocks:
		var blo = get_node( bl )
		blo.setSelected(false)
		blo.scaleTweenNode(1.0).start()
	selectedBlocks = []

# Add the selected block to the selected list. WE SHOULD FIX THIS, IT ONLY NEEDS TO STORE IT, NOT AN ARRAY!
func addSelected(bl):
	selectedBlocks.append(bl)

# Used for the multiplayer mode to force click a block on their side.
func forceClickBlock( pos ):
	shape[pos].forceClick()

func clickBlock( name ):
	#now check if that was the second block we picked. If it was, we want to
	#unselect the blocks again
	if (offClick):
		offClick = false
		addSelected(name)
		clearSelection()
	else:
		offClick = true;
		addSelected(name)

# Calculates the layer that a block is on.
# COPY OF FUNCTION IN PUZZLEMAN, IS THERE A BETTER WAY TO DO THIS?!
func calcBlockLayer( x, y, z ):
	return max( max( abs( x ), abs( y ) ), abs( z ) )
func calcBlockLayerVec( pos ):
	return max( max( abs( pos.x ), abs( pos.y ) ), abs( pos.z ) )

# Handles keeping track of pairs being removed.
func popPair( pos ):
	var blayer = calcBlockLayerVec( pos )
	puzzle.pairCount[blayer] -= 1

	if( puzzle.pairCount[blayer] == 0 ):
		print("LAYER CLEARED")
		if blayer == 1:
			print( "GAME OVER!" )
			var pauseMenu = get_tree().get_root().get_node( "Spatial" ).get_node( "Camera" ).pauseMenu
			pauseMenu.set_text("GAME OVER\nSCORE:1,000,000")
			# TODO set timeout!!!
			pauseMenu.popup_centered()

		for b in shape:
			if not ( shape[b] == null ):
				if calcBlockLayerVec( b ) == blayer:
					if shape[b].getBlockType() == BLOCK_LASER:
						shape[b].forceActivate()

		# Fire beams.
		var beamNum = 0
		for l in range( puzzle.lasers.size() ):
			if calcBlockLayerVec( puzzle.lasers[l][0] ) == blayer:
				# Firin mah lazerz!
				var beam = beamScn.instance()

				beam.set_name( str(blayer) + "_beam_" + str(beamNum) )
				beamNum += 1
				beam.set_script( Beam )

				add_child( beam )
				beam.fire( puzzle.lasers[l][0], puzzle.lasers[l][1] )

				samplePlayer.play( "soundslikewillem_hitting_slinky" )

func _init():
	# Load sound
	samplePlayer.set_voice_count(10)
	samplePlayer.set_sample_library(ResourceLoader.load("new_samplelibrary.xml"))
	print("GridMan initialized")


Adjusts expected name for animation including disallowed animation name chars.

extends ColorRect

const GOLDEN_GLTF_SCENE: PackedScene = preload("res://gltf/45545-relax-name-sanitization.gltf")

## These constants are the prefix part of the node names used in the golden glTF file.
# Prefix for "basic" nodes, no import hints, no animation, etc...
const GLTF_PREFIX_BASIC: String = "Basic_"

# Prefix for nodes that have an import hint.
const GLTF_PREFIX_HINTED: String = "Hinted_"

# Prefix for nodes that have an associated animation.
const GLTF_PREFIX_ANIMATED: String = "Animated_"

# The suffix for animations (all animations in the golden glTF file use the Blender default suffix
# "Action" and the looping import hint "-loop".
const GLTF_SUFFIX_ANIMATION: String = "Action-loop"

## These constants are the infix part of the node names used in the golden glTF file.
const GLTF_ALPHANUMERIC: String = "Alphanumeric123"
# The last three symbols are:
# https://en.wiktionary.org/wiki/%E3%82%B4#Japanese
# https://en.wiktionary.org/wiki/%E3%83%89#Japanese
# https://en.wiktionary.org/wiki/%E3%83%84#Japanese
const GLTF_KATAKANA: String = "Katakana_ゴドツ"
# The last two symbols are:
# https://emojipedia.org/grinning-face
# https://emojipedia.org/robot/
const GLTF_EMOJI: String = "Emoji_😀🤖"
const GLTF_ALLOWED_SYMBOLS: String = "AllowedSymbols_!#$%^&*()-=[]{}';<>,~"
const GLTF_DISALLOWED_NONCLASHING: String = "Disallowed_Non-clashing_.:@\"/"
const GLTF_DISALLOWED_CLASHING_1: String = "Disallowed_Clashing_."
const GLTF_DISALLOWED_CLASHING_2: String = "Disallowed_Clashing_@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_1: String = "Disallowed_Clashing_Deep_Tree_@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_2: String = "Disallowed_Clashing_Deep_Tree_@@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_3: String = "Disallowed_Clashing_Deep_Tree_@@@"

var output: BoxContainer
var test_scene: Node
var test_animation_player: AnimationPlayer

func _ready():
	output = $ScrollContainer/OutputWindow
	test_scene = GOLDEN_GLTF_SCENE.instance()
	test_animation_player = test_scene.get_node_or_null("AnimationPlayer")
	if not test_animation_player:
		_add_line("ERROR: imported glTF scene has no AnimationPlayer node!", Color.orangered)
		print("FAIL: imported glTF scene has no AnimationPlayer so no tests will be run.")
		return

	var passed_tests: Array = []
	var failed_tests: Array = []

	for method_name in _find_tests():
		_add_test_header(method_name)
		var result: bool = self.call(method_name)
		_add_line(" ")
		if result:
			passed_tests.push_back(method_name)
		else:
			failed_tests.push_back(method_name)

	if failed_tests:
		print("FAIL: %d failed tests:" % len(failed_tests))
		for test in failed_tests:
			print("\t%s" % test)
		print("")
		print("Animations:")
		for animation_name in test_animation_player.get_animation_list():
			print("\t%s" % animation_name)
	else:
		print("PASS: Passed all %d tests" % len(passed_tests))

func _find_tests() -> Array:
	var ret: Array = []
	for method in get_method_list():
		var method_name: String = method.get("name")
		if method_name.begins_with("_test_"):
			ret.push_back(method_name)
	ret.sort()
	return ret

func _add_test_header(method_name: String) -> void:
	_add_line(method_name, Color.royalblue)

func _add_result(succeeded: bool, message: String) -> void:
	var prefix: String = "  [OK] " if succeeded else "  [FAIL] "
	_add_line(prefix + message, Color.forestgreen if succeeded else Color.orangered)

func _add_line(text: String, font_color: Color = Color.black) -> void:
	var line: Label = Label.new()
	line.text = text
	line.add_theme_font_size_override("font_size", 22)
	line.add_theme_color_override("font_color", font_color)
	output.add_child(line)
	print(text)

func _check_basic(original: String, expected: String) -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false
	_add_result(true, "Found expected node '%s'" % expected)
	return true

func _test_basic_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_ALLOWED_SYMBOLS
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_KATAKANA
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_EMOJI
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_BASIC + "Disallowed_Non-clashing_"
	return _check_basic(original, expected)

func _test_basic_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_BASIC + "Disallowed_Clashing_"
	if not _check_basic(original, expected):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_BASIC + "Disallowed_Clashing_2"
	return _check_basic(original, expected)

func _test_basic_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_"
	if not _check_basic(original, expected_root):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = expected_root + "/" + GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_2"
	if not _check_basic(original, expected_mid):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = expected_mid + "/" + GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_3"
	return _check_basic(original, expected_leaf)

# Verifies that a node exists with the expected name and that it has a StaticBody3D child.
func _check_hinted_col(original: String, expected: String) -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false

	var collision_body: StaticBody3D = node.get_node_or_null("static_collision")
	if not collision_body:
		_add_result(
			false,
			"Missing 'static_collision' child for Godot node '%s' (glTF '%s')" % [
				expected, original])
		return false

	# This test assumes that if the static_collision node was created the -col hint was properly
	# handled (as that behavior should be tested elsewhere).
	_add_result(true, "Expected node '%s' contains a static_collision StaticBody3D" % expected)
	return true

func _test_hinted_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_ALLOWED_SYMBOLS
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_KATAKANA
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_EMOJI
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_HINTED + "Disallowed_Non-clashing_"
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_HINTED + "Disallowed_Clashing_"
	if not _check_hinted_col(original, expected):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_HINTED + "Disallowed_Clashing_2"
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_"
	if not _check_hinted_col(original, expected_root):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = expected_root + "/" + GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_2"
	if not _check_hinted_col(original, expected_mid):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = expected_mid + "/" + GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_3"
	return _check_hinted_col(original, expected_leaf)

# Verifies that a node exists with the expected name and that an associated animation exists.
func _check_animated(original: String, expected: String, expected_animation: String = "") -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false

	if expected_animation.is_empty():
		expected_animation = expected + GLTF_SUFFIX_ANIMATION
	var animation: Animation = test_animation_player.get_animation(expected_animation)
	if not animation:
		_add_result(
			false,
			"Missing expected animation '%s' for Godot node '%s' (glTF '%s')" % [
				expected_animation, expected, original])
		return false

	var track_count: int = animation.get_track_count()
	if track_count != 1:
		_add_result(
			false,
			"Track count %d != 1 on animation '%s' for Godot node '%s' (glTF '%s')" % [
				track_count, expected_animation, expected, original])
		return false

	var track_type: int = animation.track_get_type(0)
	if track_type != Animation.TYPE_TRANSFORM:
		_add_result(
			false,
			"Unexpected track type %d on animation '%s' for Godot node '%s' (glTF '%s')" % [
				track_type, expected_animation, expected, original])
		return false

	var track_path: NodePath = animation.track_get_path(0)
	var expected_track_path = NodePath(expected)
	if track_path != expected_track_path:
		_add_result(
			false,
			("Incorrect track_path '%s' (expected '%s') on animation '%s' for Godot node '%s' \
				 (glTF '%s')" % [
					track_path, expected_track_path, expected_animation, expected, original]))
		return false

	# This test assumes that if the static_collision node was created the -col hint was properly
	# handled (as that behavior should be tested elsewhere).
	_add_result(true, "Expected node '%s' has an associated animation" % expected)
	return true

func _test_animated_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_ALLOWED_SYMBOLS
	var expected_node: String = original
	var expected_animation: String = original.replace(",", "").replace("[", "")
	expected_animation += GLTF_SUFFIX_ANIMATION
	return _check_animated(original, expected_node, expected_animation)

func _test_animated_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_KATAKANA
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_EMOJI
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_ANIMATED + "Disallowed_Non-clashing_"
	return _check_animated(original, expected)

func _test_animated_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_"
	if not _check_animated(original, expected):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_2"
	return _check_animated(original, expected)

func _test_animated_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_"
	if not _check_animated(original, expected_root):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = (expected_root + "/" +
		GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_2")
	if not _check_animated(original, expected_mid):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = (expected_mid + "/" +
		GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_3")
	return _check_animated(original, expected_leaf)

extends ColorRect

const GOLDEN_GLTF_SCENE: PackedScene = preload("res://gltf/45545-relax-name-sanitization.gltf")

## These constants are the prefix part of the node names used in the golden glTF file.
# Prefix for "basic" nodes, no import hints, no animation, etc...
const GLTF_PREFIX_BASIC: String = "Basic_"

# Prefix for nodes that have an import hint.
const GLTF_PREFIX_HINTED: String = "Hinted_"

# Prefix for nodes that have an associated animation.
const GLTF_PREFIX_ANIMATED: String = "Animated_"

# The suffix for animations (all animations in the golden glTF file use the Blender default suffix
# "Action" and the looping import hint "-loop".
const GLTF_SUFFIX_ANIMATION: String = "Action-loop"

## These constants are the infix part of the node names used in the golden glTF file.
const GLTF_ALPHANUMERIC: String = "Alphanumeric123"
# The last three symbols are:
# https://en.wiktionary.org/wiki/%E3%82%B4#Japanese
# https://en.wiktionary.org/wiki/%E3%83%89#Japanese
# https://en.wiktionary.org/wiki/%E3%83%84#Japanese
const GLTF_KATAKANA: String = "Katakana_ゴドツ"
# The last two symbols are:
# https://emojipedia.org/grinning-face
# https://emojipedia.org/robot/
const GLTF_EMOJI: String = "Emoji_😀🤖"
const GLTF_ALLOWED_SYMBOLS: String = "AllowedSymbols_!#$%^&*()-=[]{}';<>,~"
const GLTF_DISALLOWED_NONCLASHING: String = "Disallowed_Non-clashing_.:@\"/"
const GLTF_DISALLOWED_CLASHING_1: String = "Disallowed_Clashing_."
const GLTF_DISALLOWED_CLASHING_2: String = "Disallowed_Clashing_@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_1: String = "Disallowed_Clashing_Deep_Tree_@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_2: String = "Disallowed_Clashing_Deep_Tree_@@"
const GLTF_DISALLOWED_CLASHING_DEEP_TREE_3: String = "Disallowed_Clashing_Deep_Tree_@@@"

var output: BoxContainer
var test_scene: Node
var test_animation_player: AnimationPlayer

func _ready():
	output = $ScrollContainer/OutputWindow
	test_scene = GOLDEN_GLTF_SCENE.instance()
	test_animation_player = test_scene.get_node_or_null("AnimationPlayer")
	if not test_animation_player:
		_add_line("ERROR: imported glTF scene has no AnimationPlayer node!", Color.orangered)
		print("FAIL: imported glTF scene has no AnimationPlayer so no tests will be run.")
		return

	var passed_tests: Array = []
	var failed_tests: Array = []

	for method_name in _find_tests():
		_add_test_header(method_name)
		var result: bool = self.call(method_name)
		_add_line(" ")
		if result:
			passed_tests.push_back(method_name)
		else:
			failed_tests.push_back(method_name)

	if failed_tests:
		print("FAIL: %d failed tests:" % len(failed_tests))
		for test in failed_tests:
			print("\t%s" % test)
		print("")
		print("Animations:")
		for animation_name in test_animation_player.get_animation_list():
			print("\t%s" % animation_name)
	else:
		print("PASS: Passed all %d tests" % len(passed_tests))

func _find_tests() -> Array:
	var ret: Array = []
	for method in get_method_list():
		var method_name: String = method.get("name")
		if method_name.begins_with("_test_"):
			ret.push_back(method_name)
	ret.sort()
	return ret

func _add_test_header(method_name: String) -> void:
	_add_line(method_name, Color.royalblue)

func _add_result(succeeded: bool, message: String) -> void:
	var prefix: String = "  [OK] " if succeeded else "  [FAIL] "
	_add_line(prefix + message, Color.forestgreen if succeeded else Color.orangered)

func _add_line(text: String, font_color: Color = Color.black) -> void:
	var line: Label = Label.new()
	line.text = text
	line.add_theme_font_size_override("font_size", 22)
	line.add_theme_color_override("font_color", font_color)
	output.add_child(line)

func _check_basic(original: String, expected: String) -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false
	_add_result(true, "Found expected node '%s'" % expected)
	return true

func _test_basic_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_ALLOWED_SYMBOLS
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_KATAKANA
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_EMOJI
	var expected: String = original
	return _check_basic(original, expected)

func _test_basic_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_BASIC + "Disallowed_Non-clashing_"
	return _check_basic(original, expected)

func _test_basic_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_BASIC + "Disallowed_Clashing_"
	if not _check_basic(original, expected):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_BASIC + "Disallowed_Clashing_2"
	return _check_basic(original, expected)

func _test_basic_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_"
	if not _check_basic(original, expected_root):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = expected_root + "/" + GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_2"
	if not _check_basic(original, expected_mid):
		return false

	original = GLTF_PREFIX_BASIC + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = expected_mid + "/" + GLTF_PREFIX_BASIC + "Disallowed_Clashing_Deep_Tree_3"
	return _check_basic(original, expected_leaf)

# Verifies that a node exists with the expected name and that it has a StaticBody3D child.
func _check_hinted_col(original: String, expected: String) -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false

	var collision_body: StaticBody3D = node.get_node_or_null("static_collision")
	if not collision_body:
		_add_result(
			false,
			"Missing 'static_collision' child for Godot node '%s' (glTF '%s')" % [
				expected, original])
		return false

	# This test assumes that if the static_collision node was created the -col hint was properly
	# handled (as that behavior should be tested elsewhere).
	_add_result(true, "Expected node '%s' contains a static_collision StaticBody3D" % expected)
	return true

func _test_hinted_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_ALLOWED_SYMBOLS
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_KATAKANA
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_EMOJI
	var expected: String = original
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_HINTED + "Disallowed_Non-clashing_"
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_HINTED + "Disallowed_Clashing_"
	if not _check_hinted_col(original, expected):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_HINTED + "Disallowed_Clashing_2"
	return _check_hinted_col(original, expected)

func _test_hinted_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_"
	if not _check_hinted_col(original, expected_root):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = expected_root + "/" + GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_2"
	if not _check_hinted_col(original, expected_mid):
		return false

	original = GLTF_PREFIX_HINTED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = expected_mid + "/" + GLTF_PREFIX_HINTED + "Disallowed_Clashing_Deep_Tree_3"
	return _check_hinted_col(original, expected_leaf)

# Verifies that a node exists with the expected name and that an associated animation exists.
func _check_animated(original: String, expected: String) -> bool:
	var node: Node = test_scene.get_node_or_null(expected)
	if not node:
		_add_result(
			false,
			"Missing expected Godot node '%s' for glTF node '%s'" % [expected, original])
		return false

	var expected_animation: String = expected + GLTF_SUFFIX_ANIMATION
	var animation: Animation = test_animation_player.get_animation(expected_animation)
	if not animation:
		_add_result(
			false,
			"Missing expected animation '%s' for Godot node '%s' (glTF '%s')" % [
				expected_animation, expected, original])
		return false

	var track_count: int = animation.get_track_count()
	if track_count != 1:
		_add_result(
			false,
			"Track count %d != 1 on animation '%s' for Godot node '%s' (glTF '%s')" % [
				track_count, expected_animation, expected, original])
		return false

	var track_type: int = animation.track_get_type(0)
	if track_type != Animation.TYPE_TRANSFORM:
		_add_result(
			false,
			"Unexpected track type %d on animation '%s' for Godot node '%s' (glTF '%s')" % [
				track_type, expected_animation, expected, original])
		return false

	var track_path: NodePath = animation.track_get_path(0)
	var expected_track_path = NodePath(expected)
	if track_path != expected_track_path:
		_add_result(
			false,
			("Incorrect track_path '%s' (expected '%s') on animation '%s' for Godot node '%s' \
				 (glTF '%s')" % [
					track_path, expected_track_path, expected_animation, expected, original]))
		return false

	# This test assumes that if the static_collision node was created the -col hint was properly
	# handled (as that behavior should be tested elsewhere).
	_add_result(true, "Expected node '%s' has an associated animation" % expected)
	return true

func _test_animated_alphanumeric_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_ALPHANUMERIC
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_allowed_symbols_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_ALLOWED_SYMBOLS
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_katakana_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_KATAKANA
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_emoji_is_unchanged() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_EMOJI
	var expected: String = original
	return _check_animated(original, expected)

func _test_animated_disallowed_characters_are_stripped() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_NONCLASHING
	var expected: String = GLTF_PREFIX_ANIMATED + "Disallowed_Non-clashing_"
	return _check_animated(original, expected)

func _test_animated_disallowed_clashing_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_1
	var expected: String = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_"
	if not _check_animated(original, expected):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_2
	expected = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_2"
	return _check_animated(original, expected)

func _test_animated_disallowed_nested_nodes_are_uniquified() -> bool:
	var original: String = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_1
	var expected_root: String = GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_"
	if not _check_animated(original, expected_root):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_2
	var expected_mid = (expected_root + "/" +
		GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_2")
	if not _check_animated(original, expected_mid):
		return false

	original = GLTF_PREFIX_ANIMATED + GLTF_DISALLOWED_CLASHING_DEEP_TREE_3
	var expected_leaf = (expected_mid + "/" +
		GLTF_PREFIX_ANIMATED + "Disallowed_Clashing_Deep_Tree_3")
	return _check_animated(original, expected_leaf)


use puzzle.shape

use puzzle.shape

extends Spatial

var puzzle
var puzzleMan
var DataMan = preload("res://scripts/DataManager.gd")

var gridMan
var prevBlocks = [] # keeps track of prevous color for pairs by layer
var blockColors = preload("res://scripts/PuzzleManager.gd").blockColors
var id

var saveDir = OS.get_data_dir() + "/PuzzleSaves"

var fd
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", {
		optionButt=OptionButton.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		optionButt=OptionButton.new(),
		values=blockColors,
		value="Blue"
	}],
	["action_toggle", {
		optionButt=OptionButton.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]
var action_ix = gui.size() - 1 - 1
var color_ix = gui.size() - 1 - 2
var class_ix = gui.size() - 1 - 3

# gross style, cannot , but clean enough
func shouldAddNeighbor():
	return gui[action_ix][1].value == "Add"

func shouldReplaceSelf():
	return gui[action_ix][1].value == "Replace"

func shouldRemoveSelf():
	return gui[action_ix][1].value == "Remove"

# called in AbstractBlock to add a new block
func addBlock(pos):
	var curColor = gui[color_ix][1].value
	var b = puzzleMan.PickledBlock.new() \
		.setName(id) \
		.setTextureName(curColor) \
		.setBlockPos(pos)
	var layer = puzzleMan.calcBlockLayerVec(pos)
	id += 1

	if layer == 0 and not gui[class_ix][1].value == "GOAL":
		return

	# expand prevblocks index
	while prevBlocks.size() <= layer:
		var d = {}
		for k in blockColors:
			d[k] = null
		prevBlocks.append(d)

	var pb = prevBlocks[layer][curColor]

	if pb != null:
		b.setPairName(pb.name)
		gridMan.get_node(pb.toNode().name).setPairName(b.name)
		prevBlocks[layer][curColor] = null
	else:
		# TODO SUPPORT GLYPHS HERE, SET PAIRWISE GLYPH
		prevBlocks[layer][curColor] = b

	gui[0][1].set_text(getPrevBlockErrors())
	var block = gridMan.addPickledBlock(b)
	var v = 0.01
	block.set_scale(Vector3(v,v,v))
	block.scaleTweenNode(1, 0.25, Tween.TRANS_QUART).start()
	return b

func getPrevBlockErrors():
	# hahaha, so bad
	var missing = "STATUS: "
	# check every layer
	for l in range(prevBlocks.size()):
		# check every color
		for k in prevBlocks[l].keys():
			var missed = false
			if prevBlocks[l][k] != null:
				# one message about the current layer
				if not missed:
					missing += " L" + str(l) + " "
					missed = true
				missing += k.to_upper() + " " # bad way of constructing strings
	if missing == "":
		missing += "NOMINAL"
	return missing

func changeValue(ix, togg):
	togg.value = togg.values[ix]

func _ready():
	# lights!
	get_tree().get_root().add_child(preload( "res://puzzleView.scn" ).instance())
	puzzle = preload( "res://puzzle.scn" ).instance()
	puzzle.mainPuzzle = false

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

	puzzle.set_as_toplevel(true)
	get_tree().get_root().add_child(puzzle)

	gridMan = puzzle.get_node("GridView/GridMan")
	id = gridMan.puzzle.shape.size()

	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")
	fd = initLoadSaveDialog(self, get_tree(), saveDir)

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

			# index of items needed
			e.connect("item_selected", self, "changeValue", [togg])
			for i in range(togg.values.size()):
				e.add_item(togg.values[i], i)
				# e.add_icon_item(togg.values[i], i)

		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, 'showSaveDialog', [fd, self])
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'showLoadDialog', [fd, self])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)

func puzzleSave():
	var f = fd.get_current_path()
	if f == null or f == "":
		return
	print("SAVING TO ", f)
	DataMan.savePuzzle( f, gridMan.puzzle )

func puzzleLoad():
	var f = fd.get_current_path()
	if f == null or f == "":
		return
	print("LOADING FROM ", f)
	prevBlocks = []
	gridMan.set_puzzle(DataMan.loadPuzzle( f ))

static func showLoadDialog(fileD, parent):
	if fileD.is_connected("confirmed", parent, "puzzleSave"):
		fileD.disconnect("confirmed", parent, "puzzleSave")
	fileD.connect("confirmed", parent, "puzzleLoad")
	fileD.set_mode(FileDialog.MODE_OPEN_FILE)

	fileD.popup_centered()

static func showSaveDialog(fileD, parent):
	if fileD.is_connected("confirmed", parent, "puzzleLoad"):
		fileD.disconnect("confirmed", parent, "puzzleLoad")
	fileD.connect("confirmed", parent, "puzzleSave")
	fileD.set_mode(FileDialog.MODE_SAVE_FILE)

	fileD.popup_centered()

static func initLoadSaveDialog(parent, tree, saveDir):
	# setup text in the file dialog
	var fileDialog = load("res://fileDialog.scn").instance()
	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_FILESYSTEM)
	fileDialog.add_filter("*.pzl ; Anttris Puzzle")

	Directory.new().make_dir_recursive(saveDir)
	fileDialog.set_current_dir(saveDir)
	print("Saves in ", saveDir)

	# 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", tree, "set_pause", [false])
	fileDialog.connect("hide", tree, "set_pause", [false])
	fileDialog.connect("about_to_show", tree, "set_pause", [true])
	fileDialog.hide()

	parent.add_child(fileDialog)
	fileDialog.set_owner(parent)
	return fileDialog

extends Spatial

var puzzle
var puzzleMan
var DataMan = preload("res://scripts/DataManager.gd")

var gridMan
var prevBlocks = [] # keeps track of prevous color for pairs by layer
var blockColors = preload("res://scripts/PuzzleManager.gd").blockColors
var id

var saveDir = OS.get_data_dir() + "/PuzzleSaves"

var fd
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", {
		optionButt=OptionButton.new(),
		values=["LZR", "WILD", "PAIR", "GOAL"],
		value="PAIR"
	}],
	["color_toggle", {
		optionButt=OptionButton.new(),
		values=blockColors,
		value="Blue"
	}],
	["action_toggle", {
		optionButt=OptionButton.new(),
		values=["Add", "Remove", "Replace"],
		value="Add"
	}],
	["test_pzl", Button.new()],
]
var action_ix = gui.size() - 1 - 1
var color_ix = gui.size() - 1 - 2
var class_ix = gui.size() - 1 - 3

# gross style, cannot , but clean enough
func shouldAddNeighbor():
	return gui[action_ix][1].value == "Add"

func shouldReplaceSelf():
	return gui[action_ix][1].value == "Replace"

func shouldRemoveSelf():
	return gui[action_ix][1].value == "Remove"

# called in AbstractBlock to add a new block
func addBlock(pos):
	var curColor = gui[color_ix][1].value
	var b = puzzleMan.PickledBlock.new() \
		.setName(id) \
		.setTextureName(curColor) \
		.setBlockPos(pos)
	var layer = puzzleMan.calcBlockLayerVec(pos)
	id += 1

	if layer == 0 and not gui[class_ix][1].value == "GOAL":
		return

	# expand prevblocks index
	while prevBlocks.size() <= layer:
		var d = {}
		for k in blockColors:
			d[k] = null
		prevBlocks.append(d)

	var pb = prevBlocks[layer][curColor]

	if pb != null:
		b.setPairName(pb.name)
		gridMan.get_node(pb.toNode().name).setPairName(b.name)
		prevBlocks[layer][curColor] = null
	else:
		# TODO SUPPORT GLYPHS HERE, SET PAIRWISE GLYPH
		prevBlocks[layer][curColor] = b

	gui[0][1].set_text(getPrevBlockErrors())
	var block = gridMan.addPickledBlock(b)
	var v = 0.01
	block.set_scale(Vector3(v,v,v))
	block.scaleTweenNode(1, 0.25, Tween.TRANS_QUART).start()
	return b

func getPrevBlockErrors():
	# hahaha, so bad
	var missing = "STATUS: "
	# check every layer
	for l in range(prevBlocks.size()):
		# check every color
		for k in prevBlocks[l].keys():
			var missed = false
			if prevBlocks[l][k] != null:
				# one message about the current layer
				if not missed:
					missing += " L" + str(l) + " "
					missed = true
				missing += k.to_upper() + " " # bad way of constructing strings
	if missing == "":
		missing += "NOMINAL"
	return missing

func changeValue(ix, togg):
	togg.value = togg.values[ix]

func _ready():
	# lights!
	get_tree().get_root().add_child(preload( "res://puzzleView.scn" ).instance())
	puzzle = preload( "res://puzzle.scn" ).instance()
	puzzle.mainPuzzle = false

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

	puzzle.set_as_toplevel(true)
	get_tree().get_root().add_child(puzzle)

	gridMan = puzzle.get_node("GridView/GridMan")
	id = gridMan.shape.size()

	puzzleMan = puzzle.puzzleMan

	set_process_input(true)

	var theme = preload("res://themes/MainTheme.thm")
	fd = initLoadSaveDialog(self, get_tree(), saveDir)

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

			# index of items needed
			e.connect("item_selected", self, "changeValue", [togg])
			for i in range(togg.values.size()):
				e.add_item(togg.values[i], i)
				# e.add_icon_item(togg.values[i], i)

		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, 'showSaveDialog', [fd, self])
			if control[0] == 'load_pzl' :
				e.connect('pressed', self, 'showLoadDialog', [fd, self])
			if control[0] == 'status':
				control[1].set_text('STATUS: NOMINAL')
			add_child(e)

func puzzleSave():
	var f = fd.get_current_path()
	if f == null or f == "":
		return
	print("SAVING TO ", f)
	DataMan.savePuzzle( f, gridMan.puzzle )

func puzzleLoad():
	var f = fd.get_current_path()
	if f == null or f == "":
		return
	print("LOADING FROM ", f)
	prevBlocks = []
	gridMan.set_puzzle(DataMan.loadPuzzle( f ))

static func showLoadDialog(fileD, parent):
	if fileD.is_connected("confirmed", parent, "puzzleSave"):
		fileD.disconnect("confirmed", parent, "puzzleSave")
	fileD.connect("confirmed", parent, "puzzleLoad")
	fileD.set_mode(FileDialog.MODE_OPEN_FILE)

	fileD.popup_centered()

static func showSaveDialog(fileD, parent):
	if fileD.is_connected("confirmed", parent, "puzzleLoad"):
		fileD.disconnect("confirmed", parent, "puzzleLoad")
	fileD.connect("confirmed", parent, "puzzleSave")
	fileD.set_mode(FileDialog.MODE_SAVE_FILE)

	fileD.popup_centered()

static func initLoadSaveDialog(parent, tree, saveDir):
	# setup text in the file dialog
	var fileDialog = load("res://fileDialog.scn").instance()
	fileDialog.set_title("Select Puzzle Filename")
	fileDialog.set_access(FileDialog.ACCESS_FILESYSTEM)
	fileDialog.add_filter("*.pzl ; Anttris Puzzle")

	Directory.new().make_dir_recursive(saveDir)
	fileDialog.set_current_dir(saveDir)
	print("Saves in ", saveDir)

	# 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", tree, "set_pause", [false])
	fileDialog.connect("hide", tree, "set_pause", [false])
	fileDialog.connect("about_to_show", tree, "set_pause", [true])
	fileDialog.hide()

	parent.add_child(fileDialog)
	fileDialog.set_owner(parent)
	return fileDialog


Fixed Mask UI to not show any mask if player has none of mask.

extends HBoxContainer

const mask = preload("res://mask/mask.gd")
const types = mask.types
const type_textures = mask.type_textures

var current_mask = ""
var previous_mask = ""

onready var tree = get_tree()
onready var player = tree.get_nodes_in_group("player")[0]
onready var _sprite = get_node("Sprite")

func _ready():
	set_process(true)

func _process(delta):
	if not player == null:
		current_mask = player.get_current_mask()
		if current_mask != previous_mask:
			previous_mask = current_mask
			if type_textures.has(current_mask):
				_sprite.set_texture(type_textures[ current_mask ])
			else:
				_sprite.set_texture(null)


added replace and delete hooks for editor

extends RigidBody

var name_int
var name
var pairName
var selected = false
var blockPos
var blockLayer
var blockType
const far_away_corner = Vector3(110, 110, 110)

var editor

static func nameToNodeName(n):
	return "block" + str(n)

func setName(n):
	name_int = n
	name = nameToNodeName(n)
	set_name(nameToNodeName(n))
	return self

func setPairName(n):
	pairName = nameToNodeName(n)
	return self

func setBlockLayer(n):
	blockLayer = n
	return self

func getBlockLayer():
	return blockLayer

func setBlockType( blockT ):
	blockType = blockT
	return self

func getBlockType():
	return blockType

func setSelected(sel):
	selected = sel
	return self

func forceClick():
	activate()
	get_parent().clickBlock( name )

func addNeighbor(editor, click_normal):
	var t = get_parent().get_parent().get_transform().inverse()
	var b = editor.newPickledBlock().setBlockPos(blockPos + t * click_normal)
	get_parent().add_block(b.toNode())

# catch clicks/taps
func _input_event( camera, ev, click_pos, click_normal, shape_idx ):
	if ((ev.type==InputEvent.MOUSE_BUTTON and ev.button_index==BUTTON_LEFT)
	or (ev.type==InputEvent.SCREEN_TOUCH)):
		if (get_parent().get_parent().active and ev.is_pressed()):
			print(editor)
			if editor != null:
				if editor.shouldAddNeighbor():
					addNeighbor(editor, click_normal)
				if editor.shouldReplaceSelf():
					addNeighbor(editor, 0 * click_normal)
				if editor.shouldRemoveSelf():
					remove_with_pop(self, null)
			else:
				forceClick()

# returns this block's pairNode or null
func pairActivate():
	selected = true

	# is my pair Nil?
	if pairName == null or get_parent() == null or not get_parent().has_node(pairName):
		scaleTweenNode(0.9, 0.2, Tween.TRANS_EXPO).start()
		return null

	# get my pair node
	var pairNode = get_parent().get_node(pairName)

	# enlarge if the other guy's unselected
	if not pairNode.selected:
		scaleTweenNode(1.1, 0.2, Tween.TRANS_EXPO).start()
	return pairNode

# returns a tween node that uniformly changes the object's scale
# call start() on the returned object
func scaleTweenNode(scale, time=1, transType=Tween.TRANS_BOUNCE):
	var tweenNode = newTweenNode()
	tweenNode.interpolate_method( self, "set_scale", \
		self.get_scale(), Vector3(scale, scale, scale), \
		time, transType, Tween.EASE_OUT )

	return tweenNode

# adds a tween transition node to the tree
func newTweenNode():
	var tween = Tween.new()
	add_child(tween)
	return tween

func remove_with_pop(node, key):
	get_parent().samplePlayer.play("deraj_pop_sound")
	get_parent().remove_block(self)

func _ready():
	editor = get_tree().get_root().get_node("EditorSpatial")
	set_ray_pickable(true)

extends RigidBody

var name_int
var name
var pairName
var selected = false
var blockPos
var blockLayer
var blockType
const far_away_corner = Vector3(110, 110, 110)

var editor

static func nameToNodeName(n):
	return "block" + str(n)

func setName(n):
	name_int = n
	name = nameToNodeName(n)
	set_name(nameToNodeName(n))
	return self

func setPairName(n):
	pairName = nameToNodeName(n)
	return self

func setBlockLayer(n):
	blockLayer = n
	return self

func getBlockLayer():
	return blockLayer

func setBlockType( blockT ):
	blockType = blockT
	return self

func getBlockType():
	return blockType

func setSelected(sel):
	selected = sel
	return self

func forceClick():
	activate()
	get_parent().clickBlock( name )

func addNeighbor(editor, click_normal):
	var t = get_parent().get_parent().get_transform().inverse()
	var b = editor.newPickledBlock().setBlockPos(blockPos + t * click_normal)
	get_parent().add_block(b.toNode())

# catch clicks/taps
func _input_event( camera, ev, click_pos, click_normal, shape_idx ):
	if ((ev.type==InputEvent.MOUSE_BUTTON and ev.button_index==BUTTON_LEFT)
	or (ev.type==InputEvent.SCREEN_TOUCH)):
		if (get_parent().get_parent().active and ev.is_pressed()):
			if editor != null and editor.shouldAddNeighbor():
				addNeighbor(editor, click_normal)
			else:
				forceClick()

# returns this block's pairNode or null
func pairActivate():
	selected = true

	# is my pair Nil?
	if pairName == null or get_parent() == null or not get_parent().has_node(pairName):
		scaleTweenNode(0.9, 0.2, Tween.TRANS_EXPO).start()
		return null

	# get my pair node
	var pairNode = get_parent().get_node(pairName)

	# enlarge if the other guy's unselected
	if not pairNode.selected:
		scaleTweenNode(1.1, 0.2, Tween.TRANS_EXPO).start()
	return pairNode

# returns a tween node that uniformly changes the object's scale
# call start() on the returned object
func scaleTweenNode(scale, time=1, transType=Tween.TRANS_BOUNCE):
	var tweenNode = newTweenNode()
	tweenNode.interpolate_method( self, "set_scale", \
		self.get_scale(), Vector3(scale, scale, scale), \
		time, transType, Tween.EASE_OUT )

	return tweenNode

# adds a tween transition node to the tree
func newTweenNode():
	var tween = Tween.new()
	add_child(tween)
	return tween

func remove_with_pop(node, key):
	get_parent().samplePlayer.play("deraj_pop_sound")
	get_parent().remove_block(self)

func _ready():
	editor = get_tree().get_root().get_node("Editor")
	set_ray_pickable(true)


fix wrong default direction

fix wrong default direction

extends RigidBody2D

# Member variables
var anim = ""
var siding_left = false
var jumping = false
var shooting = false

var WALK_ACCEL = 2000.0 # Higher = Better control, Lower = Sluggish
var WALK_DEACCEL = 2000.0
var WALK_MAX_VELOCITY = 300.0
var AIR_ACCEL = 300.0 # It's over 9000!
var AIR_DEACCEL = 2900.0 # Make it higher to give the player better air control, or slower to make the game more "realistic"
var JUMP_VELOCITY = 438
var STOP_JUMP_FORCE = 2000.0

var MAX_FLOOR_AIRBORNE_TIME = 0.15

var DIR_LEFT = Vector2(-1,1)
var DIR_RIGHT = Vector2(1,1)
var CURR_DIR = Vector2(1,1)
var LAST_DIR = Vector2(0,0)

var airborne_time = 1e20
var shoot_time = 1e20

var MAX_SHOOT_POSE_TIME = 0.3

var bullet = preload("res://src/actors/player/bullet.tscn")

var floor_h_velocity = 0.0
var enemy

#func beam_to(spawner):

#	var spawnerpos = get_node(spawner).get_global_pos()
#	print("Beam to active!")
#	self.set_pos(spawnerpos)

func _integrate_forces(s):
	var linear_vel = s.get_linear_velocity()
	var step = s.get_step()

	var new_anim = anim
	var new_siding_left = siding_left

	# Get the controls
	var move_left = Input.is_action_pressed("move_left")
	var move_right = Input.is_action_pressed("move_right")
	var jump = Input.is_action_pressed("jump")
	var shoot = Input.is_action_pressed("shoot")
	var spawn = Input.is_action_pressed("spawn")

	if spawn:
		var e = enemy.instance()
		var p = get_pos()
		p.y = p.y - 100
		e.set_pos(p)
		get_parent().add_child(e)

	# Deapply prev floor velocity
	linear_vel.x -= floor_h_velocity
	floor_h_velocity = 0.0

	# Find the floor (a contact with upwards facing collision normal)
	var found_floor = false
	var floor_index = -1

	for x in range(s.get_contact_count()):
		var ci = s.get_contact_local_normal(x)
		if (ci.dot(Vector2(0, -1)) > 0.6):
			found_floor = true
			floor_index = x

	# Calculate air born time in order to control when to stop jump
	# from the user releasing jump or reaching max jump height
	if (found_floor):
		airborne_time = 0.0
	else:
		airborne_time += step # Time it spent in the air

	var on_floor = airborne_time < MAX_FLOOR_AIRBORNE_TIME

	# Process jump
	if (jumping):
		if (linear_vel.y > 0):
			# Set off the jumping flag if going down
			jumping = false

		if (!jump):
			linear_vel.y += STOP_JUMP_FORCE*step

	if (on_floor):
		# Process logic when character is on floor
		if (move_left and not move_right):
			CURR_DIR = DIR_LEFT
			if (linear_vel.x > -WALK_MAX_VELOCITY):
				linear_vel.x -= WALK_ACCEL*step
				# Prevent player from exceeding max walk velocity
				if(linear_vel.x < -WALK_MAX_VELOCITY):
					linear_vel.x = -WALK_MAX_VELOCITY

		elif (move_right and not move_left):
			CURR_DIR = DIR_RIGHT
			if (linear_vel.x < WALK_MAX_VELOCITY):
				linear_vel.x += WALK_ACCEL*step
				# Prevent player from exceeding max walk velocity
				if(linear_vel.x > WALK_MAX_VELOCITY):
					linear_vel.x = WALK_MAX_VELOCITY
		else:
			var xv = abs(linear_vel.x)
			xv -= WALK_DEACCEL*step
			if (xv < 0):
				xv = 0
			linear_vel.x = sign(linear_vel.x)*xv

		# If we can, and want to JUMP - Jump!
		if (!jumping && jump):
			# Set velocity upwards
			linear_vel.y = -JUMP_VELOCITY

			# Notify code that we are jumping
			jumping = true

			# Play the player's jump sound
			if !get_node("sfx").is_active():
				get_node("sfx").play("flupp")
			# THIS IS FOR FUTURE USE - Its a statistics thing for players to see like "Hey you only jumped 20 times during the whole game..."
			# Whatever use it is, i think its a fun element to talk about :D
			# I don't care often about the "use" of things... just having it for fun is good enough ;)
			global.times_jumped = global.times_jumped +1
			# print(global.times_jumped)

			# Achievement for jumping 50 times - Increased jump height
			if (global.times_jumped > 50):
				JUMP_VELOCITY = 550
				# print("Yay! You can now jump higher")
				# TODO: Print an achievement notification message to the player

		# Check jumping
		if (jumping):
			# Set the next animation
			new_anim = "jumping"

		# Handling an idle player
		elif (abs(linear_vel.x) < 0.1): # Using a 0.1 padding for when we consider the player to be idle
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "idle_weapon"
			else:
				new_anim = "idle"

		# The player is moving (not jumping)
		else:
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "run_weapon"
			else:
				new_anim = "run"
	else:
		# Process logic when the character is in the air
		# When we are only pressing LEFT movement
		if (move_left && !move_right):
			CURR_DIR = DIR_LEFT
			if (linear_vel.x > -WALK_MAX_VELOCITY):
				# linear_vel.x = -WALK_MAX_VELOCITY
				linear_vel.x -= AIR_ACCEL*step
				if(linear_vel.x < -WALK_MAX_VELOCITY):
					linear_vel.x = -WALK_MAX_VELOCITY

		elif (move_right && !move_left):
			CURR_DIR = DIR_RIGHT
			if (linear_vel.x < WALK_MAX_VELOCITY):
				# linear_vel.x = WALK_MAX_VELOCITY
				linear_vel.x += AIR_ACCEL*step
				if(linear_vel.x > WALK_MAX_VELOCITY):
					linear_vel.x = WALK_MAX_VELOCITY
		else:
			# Deaccelerate air movement (quickly for a smooth gameplay experience)
			var x_vel = abs(linear_vel.x)
			x_vel -= AIR_DEACCEL*step
			if (x_vel < 0):
				x_vel = 0
			linear_vel.x = sign(linear_vel.x)*x_vel

		if (linear_vel.y < 0):
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "jumping_weapon"
			else:
				new_anim = "jumping"
		else:
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "falling_weapon"

			else:
				new_anim = "falling"

	# A good idea when impementing characters of all kinds,
	# compensates for physics imprecission, as well as human reaction delay.
	if (shoot and not shooting):
		shoot_time = 0
		var bi = bullet.instance()
		var ss
		#if (CURR_DIR==DIR_LEFT):
		#	ss = -1.0
		#else:
		#	ss = 1.0
		var pos = get_pos() + get_node("bullet_shoot").get_pos()*CURR_DIR

		bi.set_pos(pos)
		get_parent().add_child(bi)

		bi.set_linear_velocity(Vector2(800.0*CURR_DIR.x, -80))
		get_node("sprite/smoke").set_emitting(true)
		get_node("sfx").play("schwuit")
		PS2D.body_add_collision_exception(bi.get_rid(), get_rid()) # Make bullet and this not collide
	else:
		shoot_time += step
	# Check siding direction
	#if (linear_vel.x < 0 && move_left):
	#	new_siding_left = true
	#elif (linear_vel.x > 0 && move_right):
	#	new_siding_left = false

	# Update siding
	if (LAST_DIR!=CURR_DIR):
		if (move_left):
			get_node("sprite").set_scale(DIR_LEFT)
			LAST_DIR = DIR_LEFT
		elif(move_right):
			get_node("sprite").set_scale(DIR_RIGHT)
			LAST_DIR = DIR_RIGHT

	# Change animation
	if (new_anim != anim):
		anim = new_anim
		get_node("anim").play(anim)

	shooting = shoot

	# Apply floor velocity
	if (found_floor):
		floor_h_velocity = s.get_contact_collider_velocity_at_pos(floor_index).x
		linear_vel.x += floor_h_velocity

	# Finally, apply gravity and set back the linear velocity
	linear_vel += s.get_total_gravity()*step
	s.set_linear_velocity(linear_vel)

func _ready():
	set_process(true)
	enemy = ResourceLoader.load("res://scenes/scn3-forest/enemy.tscn")
	# Update play time, has to go into a global function at some point
func _process(delta):
	global.time_elapsed += delta
	#print(global.time_elapsed)
	if global.time_elapsed >= int(global.playtime_limit_seconds):
		#print("Time elpased!")
		#show(Popup)
		global.time_elapsed = 0

#	if !Globals.has_singleton("Facebook"):
#		return
#	var Facebook = Globals.get_singleton("Facebook")
#	var link = Globals.get("facebook/link")
#	var icon = Globals.get("facebook/icon")
#	var msg = "I just sneezed on your wall! Beat my score and Stop the Running nose!"
#	var title = "I just sneezed on your wall!"
#	Facebook.post("feed", msg, title, link, icon)

extends RigidBody2D

# Member variables
var anim = ""
var siding_left = false
var jumping = false
var shooting = false

var WALK_ACCEL = 2000.0 # Higher = Better control, Lower = Sluggish
var WALK_DEACCEL = 2000.0
var WALK_MAX_VELOCITY = 300.0
var AIR_ACCEL = 300.0 # It's over 9000!
var AIR_DEACCEL = 2900.0 # Make it higher to give the player better air control, or slower to make the game more "realistic"
var JUMP_VELOCITY = 438
var STOP_JUMP_FORCE = 2000.0

var MAX_FLOOR_AIRBORNE_TIME = 0.15

var DIR_LEFT = Vector2(-1,1)
var DIR_RIGHT = Vector2(1,1)
var CURR_DIR = Vector2(-1,1)
var LAST_DIR = Vector2(0,0)

var airborne_time = 1e20
var shoot_time = 1e20

var MAX_SHOOT_POSE_TIME = 0.3

var bullet = preload("res://src/actors/player/bullet.tscn")

var floor_h_velocity = 0.0
var enemy

#func beam_to(spawner):

#	var spawnerpos = get_node(spawner).get_global_pos()
#	print("Beam to active!")
#	self.set_pos(spawnerpos)

func _integrate_forces(s):
	var linear_vel = s.get_linear_velocity()
	var step = s.get_step()

	var new_anim = anim
	var new_siding_left = siding_left

	# Get the controls
	var move_left = Input.is_action_pressed("move_left")
	var move_right = Input.is_action_pressed("move_right")
	var jump = Input.is_action_pressed("jump")
	var shoot = Input.is_action_pressed("shoot")
	var spawn = Input.is_action_pressed("spawn")

	if spawn:
		var e = enemy.instance()
		var p = get_pos()
		p.y = p.y - 100
		e.set_pos(p)
		get_parent().add_child(e)

	# Deapply prev floor velocity
	linear_vel.x -= floor_h_velocity
	floor_h_velocity = 0.0

	# Find the floor (a contact with upwards facing collision normal)
	var found_floor = false
	var floor_index = -1

	for x in range(s.get_contact_count()):
		var ci = s.get_contact_local_normal(x)
		if (ci.dot(Vector2(0, -1)) > 0.6):
			found_floor = true
			floor_index = x

	# Calculate air born time in order to control when to stop jump
	# from the user releasing jump or reaching max jump height
	if (found_floor):
		airborne_time = 0.0
	else:
		airborne_time += step # Time it spent in the air

	var on_floor = airborne_time < MAX_FLOOR_AIRBORNE_TIME

	# Process jump
	if (jumping):
		if (linear_vel.y > 0):
			# Set off the jumping flag if going down
			jumping = false

		if (!jump):
			linear_vel.y += STOP_JUMP_FORCE*step

	if (on_floor):
		# Process logic when character is on floor
		if (move_left and not move_right):
			CURR_DIR = DIR_LEFT
			if (linear_vel.x > -WALK_MAX_VELOCITY):
				linear_vel.x -= WALK_ACCEL*step
				# Prevent player from exceeding max walk velocity
				if(linear_vel.x < -WALK_MAX_VELOCITY):
					linear_vel.x = -WALK_MAX_VELOCITY

		elif (move_right and not move_left):
			CURR_DIR = DIR_RIGHT
			if (linear_vel.x < WALK_MAX_VELOCITY):
				linear_vel.x += WALK_ACCEL*step
				# Prevent player from exceeding max walk velocity
				if(linear_vel.x > WALK_MAX_VELOCITY):
					linear_vel.x = WALK_MAX_VELOCITY
		else:
			var xv = abs(linear_vel.x)
			xv -= WALK_DEACCEL*step
			if (xv < 0):
				xv = 0
			linear_vel.x = sign(linear_vel.x)*xv

		# If we can, and want to JUMP - Jump!
		if (!jumping && jump):
			# Set velocity upwards
			linear_vel.y = -JUMP_VELOCITY

			# Notify code that we are jumping
			jumping = true

			# Play the player's jump sound
			if !get_node("sfx").is_active():
				get_node("sfx").play("flupp")
			# THIS IS FOR FUTURE USE - Its a statistics thing for players to see like "Hey you only jumped 20 times during the whole game..."
			# Whatever use it is, i think its a fun element to talk about :D
			# I don't care often about the "use" of things... just having it for fun is good enough ;)
			global.times_jumped = global.times_jumped +1
			# print(global.times_jumped)

			# Achievement for jumping 50 times - Increased jump height
			if (global.times_jumped > 50):
				JUMP_VELOCITY = 550
				# print("Yay! You can now jump higher")
				# TODO: Print an achievement notification message to the player

		# Check jumping
		if (jumping):
			# Set the next animation
			new_anim = "jumping"

		# Handling an idle player
		elif (abs(linear_vel.x) < 0.1): # Using a 0.1 padding for when we consider the player to be idle
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "idle_weapon"
			else:
				new_anim = "idle"

		# The player is moving (not jumping)
		else:
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "run_weapon"
			else:
				new_anim = "run"
	else:
		# Process logic when the character is in the air
		# When we are only pressing LEFT movement
		if (move_left && !move_right):
			CURR_DIR = DIR_LEFT
			if (linear_vel.x > -WALK_MAX_VELOCITY):
				# linear_vel.x = -WALK_MAX_VELOCITY
				linear_vel.x -= AIR_ACCEL*step
				if(linear_vel.x < -WALK_MAX_VELOCITY):
					linear_vel.x = -WALK_MAX_VELOCITY

		elif (move_right && !move_left):
			CURR_DIR = DIR_RIGHT
			if (linear_vel.x < WALK_MAX_VELOCITY):
				# linear_vel.x = WALK_MAX_VELOCITY
				linear_vel.x += AIR_ACCEL*step
				if(linear_vel.x > WALK_MAX_VELOCITY):
					linear_vel.x = WALK_MAX_VELOCITY
		else:
			# Deaccelerate air movement (quickly for a smooth gameplay experience)
			var x_vel = abs(linear_vel.x)
			x_vel -= AIR_DEACCEL*step
			if (x_vel < 0):
				x_vel = 0
			linear_vel.x = sign(linear_vel.x)*x_vel

		if (linear_vel.y < 0):
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "jumping_weapon"
			else:
				new_anim = "jumping"
		else:
			if (shoot_time < MAX_SHOOT_POSE_TIME):
				new_anim = "falling_weapon"

			else:
				new_anim = "falling"

	# A good idea when impementing characters of all kinds,
	# compensates for physics imprecission, as well as human reaction delay.
	if (shoot and not shooting):
		shoot_time = 0
		var bi = bullet.instance()
		var ss
		#if (CURR_DIR==DIR_LEFT):
		#	ss = -1.0
		#else:
		#	ss = 1.0
		var pos = get_pos() + get_node("bullet_shoot").get_pos()*CURR_DIR

		bi.set_pos(pos)
		get_parent().add_child(bi)

		bi.set_linear_velocity(Vector2(800.0*CURR_DIR.x, -80))
		get_node("sprite/smoke").set_emitting(true)
		get_node("sfx").play("schwuit")
		PS2D.body_add_collision_exception(bi.get_rid(), get_rid()) # Make bullet and this not collide
	else:
		shoot_time += step
	# Check siding direction
	#if (linear_vel.x < 0 && move_left):
	#	new_siding_left = true
	#elif (linear_vel.x > 0 && move_right):
	#	new_siding_left = false

	# Update siding
	if (LAST_DIR!=CURR_DIR):
		if (move_left):
			get_node("sprite").set_scale(DIR_LEFT)
			LAST_DIR = DIR_LEFT
		elif(move_right):
			get_node("sprite").set_scale(DIR_RIGHT)
			LAST_DIR = DIR_RIGHT

	# Change animation
	if (new_anim != anim):
		anim = new_anim
		get_node("anim").play(anim)

	shooting = shoot

	# Apply floor velocity
	if (found_floor):
		floor_h_velocity = s.get_contact_collider_velocity_at_pos(floor_index).x
		linear_vel.x += floor_h_velocity

	# Finally, apply gravity and set back the linear velocity
	linear_vel += s.get_total_gravity()*step
	s.set_linear_velocity(linear_vel)

func _ready():
	set_process(true)
	enemy = ResourceLoader.load("res://scenes/scn3-forest/enemy.tscn")
	# Update play time, has to go into a global function at some point
func _process(delta):
	global.time_elapsed += delta
	#print(global.time_elapsed)
	if global.time_elapsed >= int(global.playtime_limit_seconds):
		#print("Time elpased!")
		#show(Popup)
		global.time_elapsed = 0

#	if !Globals.has_singleton("Facebook"):
#		return
#	var Facebook = Globals.get_singleton("Facebook")
#	var link = Globals.get("facebook/link")
#	var icon = Globals.get("facebook/icon")
#	var msg = "I just sneezed on your wall! Beat my score and Stop the Running nose!"
#	var title = "I just sneezed on your wall!"
#	Facebook.post("feed", msg, title, link, icon)


Wall sliding

Wall sliding

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

var velocity
var movement_vector = [0, 0]

var AXIS_THRESHOLD = 0.15

var body_part_head
var body_part_body
var body_part_footer

func _init(bag).(bag):
    self.bag = bag

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

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

func process(delta):
    self.modify_position(delta)

func modify_position(delta):
    var x = self.apply_axis_threshold(self.movement_vector[0])
    var y = self.apply_axis_threshold(self.movement_vector[1])
    var motion = Vector2(x, y) * self.velocity * delta
    self.avatar.move(motion)
    if (self.avatar.is_colliding()):
        var n = self.avatar.get_collision_normal()
        motion = n.slide(motion)
        self.avatar.move(motion)
    self.flip(self.movement_vector[0])

func apply_axis_threshold(axis_value):
    if abs(axis_value) < self.AXIS_THRESHOLD:
        return 0
    return axis_value

func flip(direction):
    if direction == 0:
        return

    var flip_sprite = false
    if direction > 0:
        flip_sprite = true

    self.body_part_head.set_flip_h(flip_sprite)
    self.body_part_body.set_flip_h(flip_sprite)
    self.body_part_footer.set_flip_h(flip_sprite)

func reset_movement():
    self.movement_vector = [0, 0]

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

var velocity
var movement_vector = [0, 0]

var AXIS_THRESHOLD = 0.15

var body_part_head
var body_part_body
var body_part_footer

func _init(bag).(bag):
    self.bag = bag

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

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

func process(delta):
    self.modify_position(delta)

func modify_position(delta):
    var x = self.apply_axis_threshold(self.movement_vector[0])
    var y = self.apply_axis_threshold(self.movement_vector[1])
    var motion = Vector2(x, y) * self.velocity * delta
    self.avatar.move(motion)
    self.flip(self.movement_vector[0])

func apply_axis_threshold(axis_value):
    if abs(axis_value) < self.AXIS_THRESHOLD:
        return 0
    return axis_value

func flip(direction):
    if direction == 0:
        return

    var flip_sprite = false
    if direction > 0:
        flip_sprite = true

    self.body_part_head.set_flip_h(flip_sprite)
    self.body_part_body.set_flip_h(flip_sprite)
    self.body_part_footer.set_flip_h(flip_sprite)

func reset_movement():
    self.movement_vector = [0, 0]


Prevented Grumpy adds from spamming tombstones

extends "res://scripts/enemies/abstract_boss.gd"

const THRASH_AMOUNT = 2
var THRASH_SPAWN_TIME = 5
var VULNERABLE_TIME = 3.5
var RAINBOW_DELAY = 5
var thrash_template = 'fat_rat'

var shield_points = [
    Vector2(17, 3),
    Vector2(17, 8),
    Vector2(2, 8),
]
var shield_points_global = []

var shield
var angry_body = null
var rainbow_spawned = true
var rainbow = 'rainbow'

func _init(bag).(bag):
    self.avatar = preload("res://scenes/enemies/grumpy_prime.xscn").instance()
    self.body_part_head = self.avatar.get_node('body')
    self.body_part_body = self.avatar.get_node('body')
    self.body_part_footer = self.avatar.get_node('body')
    self.animations = self.avatar.get_node('body_animations')
    self.angry_body = self.avatar.get_node('angry')
    self.shield = self.avatar.get_node('shield')
    self.hit_particles = self.avatar.get_node('hitparticles')
    self.speech_bubble = self.avatar.get_node('speech')

    self.velocity = 25
    self.attack_range = 50
    self.attack_strength = 4
    self.attack_cooldown = 2
    self.max_hp = 200
    self.hp = 200
    self.score = 200
    self.is_invulnerable = true

    self.stun_duration = 0.4
    self.mass = 3

    self.phase_hp_thresholds = [
        [100, 2],
    ]

    for point in self.shield_points:
        self.shield_points_global.push_back(self.bag.room_loader.translate_position(point))

func spawn(position):
    .spawn(position)
    self.randomize_movement()
    self.raise_shield()
    self.spawn_thrash()
    self.speak("NO", 2, true)

func phase2():
    self.body_part_body.hide()
    self.angry_body.show()
    self.THRASH_SPAWN_TIME = 2.5
    self.VULNERABLE_TIME = 2
    self.RAINBOW_DELAY = 2.5
    self.velocity = 50
    self.speak("I had fun once", 2, true)

func process_ai():
    var distance

    if self.target == null:
        for player in self.bag.players.players:
            if player.is_playing && player.is_alive:
                distance = self.calculate_distance_to_object(player)
                if distance < self.aggro_range:
                    self.target = player
                    break

    if self.target != null:
        distance = self.calculate_distance_to_object(self.target)
        if not self.target.is_alive || distance > self.aggro_range:
            self.target = null
        elif distance < self.attack_range and not self.is_attack_on_cooldown:
            self.attack()

    if not self.rainbow_spawned:
        self.rainbow_spawned = true
        self.bag.timers.set_timeout(self.RAINBOW_DELAY, self, 'spawn_rainbow')

func randomize_movement():
    if self.hp < 1:
        return

    var direction

    var x = randi() % 18 + 1
    var y = randi() % 8 + 1

    direction = self.bag.room_loader.translate_position(Vector2(x, y))
    direction = self.cast_movement_vector(direction)

    self.movement_vector[0] = direction.x
    self.movement_vector[1] = direction.y

func reset_movement():
    return

func modify_position(delta):
    var x = self.movement_vector[0]
    var y = self.movement_vector[1]
    var motion = Vector2(x, y) * self.velocity * delta
    self.avatar.move(motion)
    self.flip(self.movement_vector[0])
    if (self.avatar.is_colliding()):
        var n = self.avatar.get_collision_normal()
        motion = n.slide(motion)
        self.avatar.move(motion)
        self.randomize_movement()

func external_stun(duration=null):
    if self.is_invulnerable:
        return

    .external_stun(duration)

func push_back(enemy):
    if self.is_invulnerable:
        return

    .push_back(enemy)

func schedule_thrash_spawn():
    self.bag.timers.set_timeout(self.THRASH_SPAWN_TIME, self, 'spawn_thrash')

func spawn_thrash():
    if self.hp < 1:
        return

    var position = self.avatar.get_global_pos()
    var new_guy

    for i in range(0, self.THRASH_AMOUNT):
        position = self.avatar.get_global_pos()
        position.x += pow(-1, i) * 30
        position.y += 30
        new_guy = self.bag.enemies.spawn_global(self.thrash_template, position)
        new_guy.is_attack_on_cooldown = true
        new_guy.has_tombstone = false
        new_guy.drop_chance = 0
        self.bag.timers.set_timeout(self.attack_cooldown, new_guy, "attack_cooled_down")
        new_guy.make_invulnerable(1)

    self.schedule_thrash_spawn()

func disable_shield():
    self.is_invulnerable = false
    self.shield.hide()
    self.bag.timers.set_timeout(self.VULNERABLE_TIME, self, 'raise_shield')

func raise_shield():
    self.animations.play('shield_on')
    self.bag.timers.set_timeout(0.5, self, 'raise_shield_anim_end')

func raise_shield_anim_end():
    self.is_invulnerable = true
    self.rainbow_spawned = false
    self.shield.show()

func spawn_rainbow():
    if self.hp < 1:
        return

    self.bag.enemies.spawn_global(self.rainbow, self.pick_next_point())

func pick_next_point():
    return self.shield_points_global[randi() % self.shield_points_global.size()]

func flip_body_parts(flip_flag):
    .flip_body_parts(flip_flag)
    self.angry_body.set_flip_h(flip_flag)

extends "res://scripts/enemies/abstract_boss.gd"

const THRASH_AMOUNT = 2
var THRASH_SPAWN_TIME = 5
var VULNERABLE_TIME = 3.5
var RAINBOW_DELAY = 5
var thrash_template = 'fat_rat'

var shield_points = [
    Vector2(17, 3),
    Vector2(17, 8),
    Vector2(2, 8),
]
var shield_points_global = []

var shield
var angry_body = null
var rainbow_spawned = true
var rainbow = 'rainbow'

func _init(bag).(bag):
    self.avatar = preload("res://scenes/enemies/grumpy_prime.xscn").instance()
    self.body_part_head = self.avatar.get_node('body')
    self.body_part_body = self.avatar.get_node('body')
    self.body_part_footer = self.avatar.get_node('body')
    self.animations = self.avatar.get_node('body_animations')
    self.angry_body = self.avatar.get_node('angry')
    self.shield = self.avatar.get_node('shield')
    self.hit_particles = self.avatar.get_node('hitparticles')
    self.speech_bubble = self.avatar.get_node('speech')

    self.velocity = 25
    self.attack_range = 50
    self.attack_strength = 4
    self.attack_cooldown = 2
    self.max_hp = 200
    self.hp = 200
    self.score = 200
    self.is_invulnerable = true

    self.stun_duration = 0.4
    self.mass = 3

    self.phase_hp_thresholds = [
        [100, 2],
    ]

    for point in self.shield_points:
        self.shield_points_global.push_back(self.bag.room_loader.translate_position(point))

func spawn(position):
    .spawn(position)
    self.randomize_movement()
    self.raise_shield()
    self.spawn_thrash()
    self.speak("NO", 2, true)

func phase2():
    self.body_part_body.hide()
    self.angry_body.show()
    self.THRASH_SPAWN_TIME = 2.5
    self.VULNERABLE_TIME = 2
    self.RAINBOW_DELAY = 2.5
    self.velocity = 50
    self.speak("I had fun once", 2, true)

func process_ai():
    var distance

    if self.target == null:
        for player in self.bag.players.players:
            if player.is_playing && player.is_alive:
                distance = self.calculate_distance_to_object(player)
                if distance < self.aggro_range:
                    self.target = player
                    break

    if self.target != null:
        distance = self.calculate_distance_to_object(self.target)
        if not self.target.is_alive || distance > self.aggro_range:
            self.target = null
        elif distance < self.attack_range and not self.is_attack_on_cooldown:
            self.attack()

    if not self.rainbow_spawned:
        self.rainbow_spawned = true
        self.bag.timers.set_timeout(self.RAINBOW_DELAY, self, 'spawn_rainbow')

func randomize_movement():
    if self.hp < 1:
        return

    var direction

    var x = randi() % 18 + 1
    var y = randi() % 8 + 1

    direction = self.bag.room_loader.translate_position(Vector2(x, y))
    direction = self.cast_movement_vector(direction)

    self.movement_vector[0] = direction.x
    self.movement_vector[1] = direction.y

func reset_movement():
    return

func modify_position(delta):
    var x = self.movement_vector[0]
    var y = self.movement_vector[1]
    var motion = Vector2(x, y) * self.velocity * delta
    self.avatar.move(motion)
    self.flip(self.movement_vector[0])
    if (self.avatar.is_colliding()):
        var n = self.avatar.get_collision_normal()
        motion = n.slide(motion)
        self.avatar.move(motion)
        self.randomize_movement()

func external_stun(duration=null):
    if self.is_invulnerable:
        return

    .external_stun(duration)

func push_back(enemy):
    if self.is_invulnerable:
        return

    .push_back(enemy)

func schedule_thrash_spawn():
    self.bag.timers.set_timeout(self.THRASH_SPAWN_TIME, self, 'spawn_thrash')

func spawn_thrash():
    if self.hp < 1:
        return

    var position = self.avatar.get_global_pos()
    var new_guy

    for i in range(0, self.THRASH_AMOUNT):
        position = self.avatar.get_global_pos()
        position.x += pow(-1, i) * 30
        position.y += 30
        new_guy = self.bag.enemies.spawn_global(self.thrash_template, position)
        new_guy.is_attack_on_cooldown = true
        new_guy.drop_chance = 0
        self.bag.timers.set_timeout(self.attack_cooldown, new_guy, "attack_cooled_down")
        new_guy.make_invulnerable(1)

    self.schedule_thrash_spawn()

func disable_shield():
    self.is_invulnerable = false
    self.shield.hide()
    self.bag.timers.set_timeout(self.VULNERABLE_TIME, self, 'raise_shield')

func raise_shield():
    self.animations.play('shield_on')
    self.bag.timers.set_timeout(0.5, self, 'raise_shield_anim_end')

func raise_shield_anim_end():
    self.is_invulnerable = true
    self.rainbow_spawned = false
    self.shield.show()

func spawn_rainbow():
    if self.hp < 1:
        return

    self.bag.enemies.spawn_global(self.rainbow, self.pick_next_point())

func pick_next_point():
    return self.shield_points_global[randi() % self.shield_points_global.size()]

func flip_body_parts(flip_flag):
    .flip_body_parts(flip_flag)
    self.angry_body.set_flip_h(flip_flag)