├── .gitignore ├── icon.png ├── addons └── gut │ ├── icon.png │ ├── source_code_pro.fnt │ ├── plugin.cfg │ ├── gut_plugin.gd │ ├── icon.png.import │ ├── stub_params.gd │ ├── thing_counter.gd │ ├── LICENSE.md │ ├── logger.gd │ ├── spy.gd │ ├── utils.gd │ ├── summary.gd │ ├── stubber.gd │ ├── signal_watcher.gd │ ├── method_maker.gd │ ├── test_collector.gd │ ├── optparse.gd │ ├── GutScene.gd │ ├── doubler.gd │ ├── gut_cmdln.gd │ ├── gut_gui.gd │ └── GutScene.tscn ├── default_env.tres ├── project.godot ├── test ├── test.tscn └── unit │ ├── test_hexgrid.gd │ ├── test_hexcell.gd │ └── test_pathfinding.gd ├── icon.png.import ├── demo_2d.gd ├── LICENSE.txt ├── demo_3d.gd ├── demo_2d.tscn ├── demo_3d.tscn ├── HexCell.gd ├── README.md └── HexGrid.gd /.gitignore: -------------------------------------------------------------------------------- 1 | .import 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romlok/godot-gdhexgrid/HEAD/icon.png -------------------------------------------------------------------------------- /addons/gut/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romlok/godot-gdhexgrid/HEAD/addons/gut/icon.png -------------------------------------------------------------------------------- /addons/gut/source_code_pro.fnt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romlok/godot-gdhexgrid/HEAD/addons/gut/source_code_pro.fnt -------------------------------------------------------------------------------- /addons/gut/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Gut" 4 | description="Unit Testing tool for Godot." 5 | author="Butch Wesley" 6 | version="6.7.0" 7 | script="gut_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/gut/gut_plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | # Initialization of the plugin goes here 6 | # Add the new type with a name, a parent type, a script and an icon 7 | add_custom_type("Gut", "Control", preload("gut.gd"), preload("icon.png")) 8 | 9 | func _exit_tree(): 10 | # Clean-up of the plugin goes here 11 | # Always remember to remove it from the engine when deactivated 12 | remove_custom_type("Gut") 13 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) 5 | sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) 6 | sky_curve = 0.25 7 | ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) 8 | ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) 9 | ground_curve = 0.01 10 | 11 | [resource] 12 | background_mode = 2 13 | background_sky = SubResource( 1 ) 14 | 15 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="HexGrid" 19 | run/main_scene="res://demo_2d.tscn" 20 | config/icon="res://icon.png" 21 | 22 | [editor_plugins] 23 | 24 | enabled=PoolStringArray( "gut" ) 25 | 26 | [rendering] 27 | 28 | environment/default_environment="res://default_env.tres" 29 | -------------------------------------------------------------------------------- /test/test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/gut/gut.gd" type="Script" id=1] 4 | [ext_resource path="res://addons/gut/icon.png" type="Texture" id=2] 5 | 6 | [node name="Node" type="Node"] 7 | 8 | [node name="Gut" type="WindowDialog" parent="."] 9 | visible = true 10 | self_modulate = Color( 1, 1, 1, 0 ) 11 | margin_right = 851.0 12 | margin_bottom = 496.0 13 | rect_min_size = Vector2( 740, 250 ) 14 | script = ExtResource( 1 ) 15 | __meta__ = { 16 | "_editor_icon": ExtResource( 2 ) 17 | } 18 | _select_script = null 19 | _tests_like = null 20 | _run_on_load = true 21 | _yield_between_tests = false 22 | _directory1 = "res://test/unit" 23 | _directory2 = "res://test/integration" 24 | _double_strategy = 1 25 | 26 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/gut/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/gut/icon.png" 13 | dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/gut/stub_params.gd: -------------------------------------------------------------------------------- 1 | var return_val = null 2 | var stub_target = null 3 | var target_subpath = null 4 | var parameters = null 5 | var stub_method = null 6 | const NOT_SET = '|_1_this_is_not_set_1_|' 7 | 8 | func _init(target=null, method=null, subpath=null): 9 | stub_target = target 10 | stub_method = method 11 | target_subpath = subpath 12 | 13 | func to_return(val): 14 | return_val = val 15 | return self 16 | 17 | func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET): 18 | parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] 19 | var idx = 0 20 | while(idx < parameters.size()): 21 | if(str(parameters[idx]) == NOT_SET): 22 | parameters.remove(idx) 23 | else: 24 | idx += 1 25 | return self 26 | 27 | func to_s(): 28 | return str(stub_target, '(', target_subpath, ').', stub_method, ' with (', parameters, ') = ', return_val) 29 | -------------------------------------------------------------------------------- /demo_2d.gd: -------------------------------------------------------------------------------- 1 | # Script to attach to a node which represents a hex grid 2 | extends Node2D 3 | 4 | var HexGrid = preload("./HexGrid.gd").new() 5 | 6 | onready var highlight = get_node("Highlight") 7 | onready var area_coords = get_node("Highlight/AreaCoords") 8 | onready var hex_coords = get_node("Highlight/HexCoords") 9 | 10 | 11 | func _ready(): 12 | HexGrid.hex_scale = Vector2(50, 50) 13 | 14 | 15 | func _unhandled_input(event): 16 | if 'position' in event: 17 | var relative_pos = self.transform.affine_inverse() * event.position 18 | # Display the coords used 19 | if area_coords != null: 20 | area_coords.text = str(relative_pos) 21 | if hex_coords != null: 22 | hex_coords.text = str(HexGrid.get_hex_at(relative_pos).axial_coords) 23 | 24 | # Snap the highlight to the nearest grid cell 25 | if highlight != null: 26 | highlight.position = HexGrid.get_hex_center(HexGrid.get_hex_at(relative_pos)) 27 | -------------------------------------------------------------------------------- /addons/gut/thing_counter.gd: -------------------------------------------------------------------------------- 1 | var things = {} 2 | 3 | func get_unique_count(): 4 | return things.size() 5 | 6 | func add(thing): 7 | if(things.has(thing)): 8 | things[thing] += 1 9 | else: 10 | things[thing] = 1 11 | 12 | func has(thing): 13 | return things.has(thing) 14 | 15 | func get(thing): 16 | var to_return = 0 17 | if(things.has(thing)): 18 | to_return = things[thing] 19 | return to_return 20 | 21 | func sum(): 22 | var count = 0 23 | for key in things: 24 | count += things[key] 25 | return count 26 | 27 | func to_s(): 28 | var to_return = "" 29 | for key in things: 30 | to_return += str(key, ": ", things[key], "\n") 31 | to_return += str("sum: ", sum()) 32 | return to_return 33 | 34 | func get_max_count(): 35 | var max_val = null 36 | for key in things: 37 | if(max_val == null or things[key] > max_val): 38 | max_val = things[key] 39 | return max_val 40 | 41 | func add_array_items(array): 42 | for i in range(array.size()): 43 | add(array[i]) 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 Mel Collins 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /demo_3d.gd: -------------------------------------------------------------------------------- 1 | # Script to attach to a node which represents a hex grid 2 | extends Spatial 3 | 4 | var HexGrid = preload("./HexGrid.gd").new() 5 | 6 | onready var highlight = get_node("Highlight") 7 | onready var plane_coords_label = get_node("Highlight/Viewport/PlaneCoords") 8 | onready var hex_coords_label = get_node("Highlight/Viewport/HexCoords") 9 | 10 | 11 | func _on_HexGrid_input_event(_camera, _event, click_position, _click_normal, _shape_idx): 12 | # It's called click_position, but you don't need to click 13 | var plane_coords = self.transform.affine_inverse() * click_position 14 | plane_coords = Vector2(plane_coords.x, plane_coords.z) 15 | # Display the coords used 16 | if plane_coords_label != null: 17 | plane_coords_label.text = str(plane_coords) 18 | if hex_coords_label != null: 19 | hex_coords_label.text = str(HexGrid.get_hex_at(plane_coords).axial_coords) 20 | 21 | # Snap the highlight to the nearest grid cell 22 | if highlight != null: 23 | var plane_pos = HexGrid.get_hex_center(HexGrid.get_hex_at(plane_coords)) 24 | highlight.translation.x = plane_pos.x 25 | highlight.translation.z = plane_pos.y 26 | -------------------------------------------------------------------------------- /addons/gut/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2018 Tom "Butch" Wesley 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /demo_2d.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://demo_2d.gd" type="Script" id=1] 4 | 5 | [sub_resource type="RectangleShape2D" id=1] 6 | extents = Vector2( 512, 300 ) 7 | 8 | [node name="2D Demo" type="Node"] 9 | 10 | [node name="Area2D" type="Area2D" parent="."] 11 | position = Vector2( 512, 300 ) 12 | script = ExtResource( 1 ) 13 | 14 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] 15 | position = Vector2( 0.237854, 0 ) 16 | shape = SubResource( 1 ) 17 | 18 | [node name="Highlight" type="Polygon2D" parent="Area2D"] 19 | polygon = PoolVector2Array( -12.5, 21.6506, 12.5, 21.6506, 25, 0, 12.5, -21.6506, -12.5, -21.6506, -25, 0 ) 20 | 21 | [node name="Label" type="Label" parent="Area2D/Highlight"] 22 | margin_left = 5.0 23 | margin_top = -39.0 24 | margin_right = 52.0 25 | margin_bottom = -25.0 26 | text = "SCREEN" 27 | 28 | [node name="AreaCoords" type="Label" parent="Area2D/Highlight"] 29 | margin_left = 55.0 30 | margin_top = -39.0 31 | margin_right = 105.0 32 | margin_bottom = -25.0 33 | text = "SCREEN" 34 | 35 | [node name="Label2" type="Label" parent="Area2D/Highlight"] 36 | margin_left = 25.0 37 | margin_top = -19.0 38 | margin_right = 56.0 39 | margin_bottom = -5.0 40 | text = "HEX" 41 | 42 | [node name="HexCoords" type="Label" parent="Area2D/Highlight"] 43 | margin_left = 55.0 44 | margin_top = -19.0 45 | margin_right = 105.0 46 | margin_bottom = -5.0 47 | text = "HEX" 48 | 49 | -------------------------------------------------------------------------------- /addons/gut/logger.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | var _gut = null 4 | 5 | var types = { 6 | warn = 'WARNING', 7 | error = 'ERROR', 8 | info = 'INFO', 9 | debug = 'DEBUG', 10 | deprecated = 'DEPRECATED' 11 | } 12 | 13 | var _logs = { 14 | types.warn: [], 15 | types.error: [], 16 | types.info: [], 17 | types.debug: [], 18 | types.deprecated: [] 19 | } 20 | 21 | var _suppress_output = false 22 | 23 | func _gut_log_level_for_type(log_type): 24 | if(log_type == types.warn or log_type == types.error or log_type == types.deprecated): 25 | return 0 26 | else: 27 | return 2 28 | 29 | func _log(type, text): 30 | _logs[type].append(text) 31 | var formatted = str('[', type, '] ', text) 32 | if(!_suppress_output): 33 | if(_gut): 34 | # this will keep the text indented under test for readability 35 | _gut.p(formatted, _gut_log_level_for_type(type)) 36 | # IDEA! We could store the current script and test that generated 37 | # this output, which could be useful later if we printed out a summary. 38 | else: 39 | print(formatted) 40 | return formatted 41 | 42 | # --------------- 43 | # Get Methods 44 | # --------------- 45 | func get_warnings(): 46 | return get_log_entries(types.warn) 47 | 48 | func get_errors(): 49 | return get_log_entries(types.error) 50 | 51 | func get_infos(): 52 | return get_log_entries(types.info) 53 | 54 | func get_debugs(): 55 | return get_log_entries(types.debug) 56 | 57 | func get_deprecated(): 58 | return get_log_entries(types.deprecated) 59 | 60 | func get_count(log_type=null): 61 | var count = 0 62 | if(log_type == null): 63 | for key in _logs: 64 | count += _logs[key].size() 65 | else: 66 | count = _logs[log_type].size() 67 | return count 68 | 69 | func get_log_entries(log_type): 70 | return _logs[log_type] 71 | 72 | # --------------- 73 | # Log methods 74 | # --------------- 75 | func warn(text): 76 | return _log(types.warn, text) 77 | 78 | func error(text): 79 | return _log(types.error, text) 80 | 81 | func info(text): 82 | return _log(types.info, text) 83 | 84 | func debug(text): 85 | return _log(types.debug, text) 86 | 87 | # supply some text or the name of the deprecated method and the replacement. 88 | func deprecated(text, alt_method=null): 89 | var msg = text 90 | if(alt_method): 91 | msg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.') 92 | return _log(types.deprecated, msg) 93 | 94 | # --------------- 95 | # Misc 96 | # --------------- 97 | func get_gut(): 98 | return _gut 99 | 100 | func set_gut(gut): 101 | _gut = gut 102 | 103 | func clear(): 104 | for key in _logs: 105 | _logs[key].clear() 106 | -------------------------------------------------------------------------------- /addons/gut/spy.gd: -------------------------------------------------------------------------------- 1 | # { 2 | # instance_id_or_path1:{ 3 | # method1:[ [p1, p2], [p1, p2] ], 4 | # method2:[ [p1, p2], [p1, p2] ] 5 | # }, 6 | # instance_id_or_path1:{ 7 | # method1:[ [p1, p2], [p1, p2] ], 8 | # method2:[ [p1, p2], [p1, p2] ] 9 | # }, 10 | # } 11 | var _calls = {} 12 | var _utils = load('res://addons/gut/utils.gd').new() 13 | var _lgr = _utils.get_logger() 14 | 15 | func _get_params_as_string(params): 16 | var to_return = '' 17 | if(params == null): 18 | return '' 19 | 20 | for i in range(params.size()): 21 | if(params[i] == null): 22 | to_return += 'null' 23 | else: 24 | if(typeof(params[i]) == TYPE_STRING): 25 | to_return += str('"', params[i], '"') 26 | else: 27 | to_return += str(params[i]) 28 | if(i != params.size() -1): 29 | to_return += ', ' 30 | return to_return 31 | 32 | func add_call(variant, method_name, parameters=null): 33 | if(!_calls.has(variant)): 34 | _calls[variant] = {} 35 | 36 | if(!_calls[variant].has(method_name)): 37 | _calls[variant][method_name] = [] 38 | 39 | _calls[variant][method_name].append(parameters) 40 | 41 | func was_called(variant, method_name, parameters=null): 42 | var to_return = false 43 | if(_calls.has(variant) and _calls[variant].has(method_name)): 44 | if(parameters): 45 | to_return = _calls[variant][method_name].has(parameters) 46 | else: 47 | to_return = true 48 | return to_return 49 | 50 | func get_call_parameters(variant, method_name, index=-1): 51 | var to_return = null 52 | var get_index = -1 53 | 54 | if(_calls.has(variant) and _calls[variant].has(method_name)): 55 | var call_size = _calls[variant][method_name].size() 56 | if(index == -1): 57 | # get the most recent call by default 58 | get_index = call_size -1 59 | else: 60 | get_index = index 61 | 62 | if(get_index < call_size): 63 | to_return = _calls[variant][method_name][get_index] 64 | else: 65 | _lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls: ', call_size)) 66 | 67 | return to_return 68 | 69 | func call_count(instance, method_name, parameters=null): 70 | var to_return = 0 71 | 72 | if(was_called(instance, method_name)): 73 | if(parameters): 74 | for i in range(_calls[instance][method_name].size()): 75 | if(_calls[instance][method_name][i] == parameters): 76 | to_return += 1 77 | else: 78 | to_return = _calls[instance][method_name].size() 79 | return to_return 80 | 81 | func clear(): 82 | _calls = {} 83 | 84 | func get_call_list_as_string(instance): 85 | var to_return = '' 86 | if(_calls.has(instance)): 87 | for method in _calls[instance]: 88 | for i in range(_calls[instance][method].size()): 89 | to_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), ")\n") 90 | return to_return 91 | 92 | func get_logger(): 93 | return _lgr 94 | 95 | func set_logger(logger): 96 | _lgr = logger 97 | -------------------------------------------------------------------------------- /test/unit/test_hexgrid.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | var HexCell = load("res://HexCell.gd") 4 | var HexGrid = load("res://HexGrid.gd") 5 | var cell 6 | var grid 7 | var w 8 | var h 9 | 10 | func setup(): 11 | cell = HexCell.new() 12 | grid = HexGrid.new() 13 | w = grid.hex_size.x 14 | h = grid.hex_size.y 15 | 16 | 17 | func test_hex_to_projection(): 18 | var tests = { 19 | # Remember, projection +y => S 20 | Vector2(0, 0): Vector2(0, 0), 21 | Vector2(0, 1): Vector2(0, -h), 22 | Vector2(1, 0): Vector2(w*0.75, -h/2), 23 | Vector2(-4, -3): Vector2(4 * (-w*0.75), (3 * h) + (4 * h / 2)), 24 | } 25 | for hex in tests: 26 | assert_eq(tests[hex], grid.get_hex_center(hex)) 27 | 28 | func test_hex_to_projection_scaled(): 29 | grid.set_hex_scale(Vector2(2, 2)) 30 | var tests = { 31 | Vector2(0, 0): Vector2(0, 0), 32 | Vector2(0, 1): 2 * Vector2(0, -h), 33 | Vector2(1, 0): 2 * Vector2(w * 0.75, -h/2), 34 | Vector2(-4, -3): 2 * Vector2(4 * (-w * 0.75), (3 * h) + (4 * h / 2)), 35 | } 36 | for hex in tests: 37 | assert_eq(tests[hex], grid.get_hex_center(hex)) 38 | 39 | func test_hex_to_projection_squished(): 40 | grid.set_hex_scale(Vector2(2, 1)) 41 | var tests = { 42 | Vector2(0, 0): Vector2(0, 0), 43 | Vector2(0, 1): Vector2(0, -h), 44 | Vector2(1, 0): Vector2(2 * w * 0.75, -h/2), 45 | Vector2(-4, -3): Vector2(2 * 4 * (-w * 0.75), (3 * h) + (4 * h / 2)), 46 | } 47 | for hex in tests: 48 | assert_eq(tests[hex], grid.get_hex_center(hex)) 49 | 50 | func test_hex_to_3d_projection(): 51 | var tests = { 52 | Vector2(0, 0): Vector3(0, 0, 0), 53 | Vector2(0, 1): Vector3(0, 0, -h), 54 | Vector2(1, 0): Vector3(w*0.75, 0, -h/2), 55 | Vector2(-4, -3): Vector3(4 * (-w*0.75), 0, (3 * h) + (4 * h / 2)), 56 | } 57 | for hex in tests: 58 | assert_eq(tests[hex], grid.get_hex_center3(hex)) 59 | # Also test the second parameter 60 | assert_eq( 61 | Vector3(0, 1.2, 0), 62 | grid.get_hex_center3(Vector2(0, 0), 1.2) 63 | ) 64 | 65 | 66 | func test_projection_to_hex(): 67 | var tests = { 68 | Vector2(0, 0): Vector2(0, 0), 69 | Vector2(w / 2 - 0.01, 0): Vector2(0, 0), 70 | Vector2(w / 2 - 0.01, h / 2): Vector2(1, -1), 71 | Vector2(w / 2 - 0.01, -h / 2): Vector2(1, 0), 72 | Vector2(0, h): Vector2(0, -1), 73 | Vector2(-w - 0.01, 0): Vector2(-2, 1), 74 | Vector2(-w, 0.01): Vector2(-1, 0), 75 | Vector2(-w, -0.01): Vector2(-1, 1), 76 | # Also Vector3s are valid input 77 | Vector3(0, 0, 0): Vector2(0, 0), 78 | Vector3(w / 2 - 0.01, 12, h / 2): Vector2(1, -1), 79 | } 80 | for coords in tests: 81 | assert_eq(tests[coords], grid.get_hex_at(coords).axial_coords) 82 | 83 | func test_projection_to_hex_doublesquished(): 84 | grid.set_hex_scale(Vector2(4, 2)) 85 | var tests = { 86 | Vector2(0, 0): Vector2(0, 0), 87 | Vector2(4 * w / 2 - 0.01, 0): Vector2(0, 0), 88 | Vector2(4 * w / 2 - 0.01, h / 2): Vector2(1, -1), 89 | Vector2(4 * w / 2 - 0.01, -h / 2): Vector2(1, 0), 90 | Vector2(0, 2 * h): Vector2(0, -1), 91 | Vector2(4 * -w - 0.01, 0): Vector2(-2, 1), 92 | Vector2(4 * -w, 0.01): Vector2(-1, 0), 93 | Vector2(4 * -w, -0.01): Vector2(-1, 1), 94 | } 95 | for coords in tests: 96 | assert_eq(tests[coords], grid.get_hex_at(coords).axial_coords) 97 | 98 | -------------------------------------------------------------------------------- /addons/gut/utils.gd: -------------------------------------------------------------------------------- 1 | var _Logger = load('res://addons/gut/logger.gd') # everything should use get_logger 2 | 3 | var Doubler = load('res://addons/gut/doubler.gd') 4 | var MethodMaker = load('res://addons/gut/method_maker.gd') 5 | var Spy = load('res://addons/gut/spy.gd') 6 | var Stubber = load('res://addons/gut/stubber.gd') 7 | var StubParams = load('res://addons/gut/stub_params.gd') 8 | var Summary = load('res://addons/gut/summary.gd') 9 | var Test = load('res://addons/gut/test.gd') 10 | var TestCollector = load('res://addons/gut/test_collector.gd') 11 | var ThingCounter = load('res://addons/gut/thing_counter.gd') 12 | 13 | const GUT_METADATA = '__gut_metadata_' 14 | 15 | enum DOUBLE_STRATEGY{ 16 | FULL, 17 | PARTIAL 18 | } 19 | 20 | var _file_checker = File.new() 21 | 22 | func is_version_30(): 23 | var info = Engine.get_version_info() 24 | return info.major == 3 and info.minor == 0 25 | 26 | func is_version_31(): 27 | var info = Engine.get_version_info() 28 | return info.major == 3 and info.minor == 1 29 | 30 | # ------------------------------------------------------------------------------ 31 | # Everything should get a logger through this. 32 | # 33 | # Eventually I want to make this get a single instance of a logger but I'm not 34 | # sure how to do that without everything having to be in the tree which I 35 | # DO NOT want to to do. I'm thinking of writings some instance ids to a file 36 | # and loading them in the _init for this. 37 | # ------------------------------------------------------------------------------ 38 | func get_logger(): 39 | return _Logger.new() 40 | 41 | # ------------------------------------------------------------------------------ 42 | # Returns an array created by splitting the string by the delimiter 43 | # ------------------------------------------------------------------------------ 44 | func split_string(to_split, delim): 45 | var to_return = [] 46 | 47 | var loc = to_split.find(delim) 48 | while(loc != -1): 49 | to_return.append(to_split.substr(0, loc)) 50 | to_split = to_split.substr(loc + 1, to_split.length() - loc) 51 | loc = to_split.find(delim) 52 | to_return.append(to_split) 53 | return to_return 54 | 55 | # ------------------------------------------------------------------------------ 56 | # Returns a string containing all the elements in the array seperated by delim 57 | # ------------------------------------------------------------------------------ 58 | func join_array(a, delim): 59 | var to_return = '' 60 | for i in range(a.size()): 61 | to_return += str(a[i]) 62 | if(i != a.size() -1): 63 | to_return += str(delim) 64 | return to_return 65 | 66 | # ------------------------------------------------------------------------------ 67 | # return if_null if value is null otherwise return value 68 | # ------------------------------------------------------------------------------ 69 | func nvl(value, if_null): 70 | if(value == null): 71 | return if_null 72 | else: 73 | return value 74 | 75 | # ------------------------------------------------------------------------------ 76 | # returns true if the object has been freed, false if not 77 | # 78 | # From what i've read, the weakref approach should work. It seems to work most 79 | # of the time but sometimes it does not catch it. The str comparison seems to 80 | # fill in the gaps. I've not seen any errors after adding that check. 81 | # ------------------------------------------------------------------------------ 82 | func is_freed(obj): 83 | var wr = weakref(obj) 84 | return !(wr.get_ref() and str(obj) != '[Deleted Object]') 85 | 86 | func is_not_freed(obj): 87 | return !is_freed(obj) 88 | 89 | func is_double(obj): 90 | return obj.get(GUT_METADATA) != null 91 | 92 | func extract_property_from_array(source, property): 93 | var to_return = [] 94 | for i in (source.size()): 95 | to_return.append(source[i].get(property)) 96 | return to_return 97 | 98 | func file_exists(path): 99 | return _file_checker.file_exists(path) 100 | 101 | func write_file(path, content): 102 | var f = File.new() 103 | f.open(path, f.WRITE) 104 | f.store_string(content) 105 | f.close() 106 | 107 | func is_null_or_empty(text): 108 | return text == null or text == '' 109 | -------------------------------------------------------------------------------- /addons/gut/summary.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # ------------------------------------------------------------------------------ 3 | class Test: 4 | var pass_texts = [] 5 | var fail_texts = [] 6 | var pending_texts = [] 7 | 8 | func to_s(): 9 | var pad = ' ' 10 | var to_return = '' 11 | for i in range(fail_texts.size()): 12 | to_return += str(pad, 'FAILED: ', fail_texts[i], "\n") 13 | for i in range(pending_texts.size()): 14 | to_return += str(pad, 'Pending: ', pending_texts[i], "\n") 15 | return to_return 16 | 17 | # ------------------------------------------------------------------------------ 18 | # ------------------------------------------------------------------------------ 19 | class TestScript: 20 | var name = 'NOT_SET' 21 | var _tests = {} 22 | var _test_order = [] 23 | 24 | func _init(script_name): 25 | name = script_name 26 | 27 | func get_pass_count(): 28 | var count = 0 29 | for key in _tests: 30 | count += _tests[key].pass_texts.size() 31 | return count 32 | 33 | func get_fail_count(): 34 | var count = 0 35 | for key in _tests: 36 | count += _tests[key].fail_texts.size() 37 | return count 38 | 39 | func get_pending_count(): 40 | var count = 0 41 | for key in _tests: 42 | count += _tests[key].pending_texts.size() 43 | return count 44 | 45 | func get_test_obj(name): 46 | if(!_tests.has(name)): 47 | _tests[name] = Test.new() 48 | _test_order.append(name) 49 | return _tests[name] 50 | 51 | func add_pass(test_name, reason): 52 | var t = get_test_obj(test_name) 53 | t.pass_texts.append(reason) 54 | 55 | func add_fail(test_name, reason): 56 | var t = get_test_obj(test_name) 57 | t.fail_texts.append(reason) 58 | 59 | func add_pending(test_name, reason): 60 | var t = get_test_obj(test_name) 61 | t.pending_texts.append(reason) 62 | 63 | # ------------------------------------------------------------------------------ 64 | # Main class 65 | # ------------------------------------------------------------------------------ 66 | var _scripts = [] 67 | 68 | func add_script(name): 69 | _scripts.append(TestScript.new(name)) 70 | 71 | func get_scripts(): 72 | return _scripts 73 | 74 | func get_current_script(): 75 | return _scripts[_scripts.size() - 1] 76 | 77 | func add_test(test_name): 78 | get_current_script().get_test_obj(test_name) 79 | 80 | func add_pass(test_name, reason = ''): 81 | get_current_script().add_pass(test_name, reason) 82 | 83 | func add_fail(test_name, reason = ''): 84 | get_current_script().add_fail(test_name, reason) 85 | 86 | func add_pending(test_name, reason = ''): 87 | get_current_script().add_pending(test_name, reason) 88 | 89 | func get_test_text(test_name): 90 | return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s() 91 | 92 | # Gets the count of unique script names minus the . at the 93 | # end. Used for displaying the number of scripts without including all the 94 | # Inner Classes. 95 | func get_non_inner_class_script_count(): 96 | var count = 0 97 | var unique_scripts = {} 98 | for i in range(_scripts.size()): 99 | var ext_loc = _scripts[i].name.find_last('.gd.') 100 | if(ext_loc == -1): 101 | unique_scripts[_scripts[i].name] = 1 102 | else: 103 | unique_scripts[_scripts[i].name.substr(0, ext_loc + 3)] = 1 104 | return unique_scripts.keys().size() 105 | 106 | func get_totals(): 107 | var totals = { 108 | passing = 0, 109 | pending = 0, 110 | failing = 0, 111 | tests = 0, 112 | scripts = 0 113 | } 114 | 115 | for s in range(_scripts.size()): 116 | totals.passing += _scripts[s].get_pass_count() 117 | totals.pending += _scripts[s].get_pending_count() 118 | totals.failing += _scripts[s].get_fail_count() 119 | totals.tests += _scripts[s]._test_order.size() 120 | 121 | totals.scripts = get_non_inner_class_script_count() 122 | 123 | return totals 124 | 125 | func get_summary_text(): 126 | var _totals = get_totals() 127 | 128 | var to_return = '' 129 | for s in range(_scripts.size()): 130 | if(_scripts[s].get_fail_count() > 0 or _scripts[s].get_pending_count() > 0): 131 | to_return += _scripts[s].name + "\n" 132 | for t in range(_scripts[s]._test_order.size()): 133 | var tname = _scripts[s]._test_order[t] 134 | var test = _scripts[s].get_test_obj(tname) 135 | if(test.fail_texts.size() > 0 or test.pending_texts.size() > 0): 136 | to_return += str(' - ', tname, "\n", test.to_s()) 137 | 138 | var header = "*** Totals ***\n" 139 | header += str(' scripts: ', get_non_inner_class_script_count(), "\n") 140 | header += str(' tests: ', _totals.tests, "\n") 141 | header += str(' passing asserts: ', _totals.passing, "\n") 142 | header += str(' failing asserts: ',_totals.failing, "\n") 143 | header += str(' pending: ', _totals.pending, "\n") 144 | 145 | return to_return + "\n" + header 146 | -------------------------------------------------------------------------------- /addons/gut/stubber.gd: -------------------------------------------------------------------------------- 1 | # { 2 | # inst_id_or_path1:{ 3 | # method_name1: [StubParams, StubParams], 4 | # method_name2: [StubParams, StubParams] 5 | # }, 6 | # inst_id_or_path2:{ 7 | # method_name1: [StubParams, StubParams], 8 | # method_name2: [StubParams, StubParams] 9 | # } 10 | # } 11 | var returns = {} 12 | var _utils = load('res://addons/gut/utils.gd').new() 13 | var _lgr = _utils.get_logger() 14 | 15 | func _is_instance(obj): 16 | return typeof(obj) == TYPE_OBJECT and !obj.has_method('new') 17 | 18 | func _make_key_from_metadata(doubled): 19 | var to_return = doubled.__gut_metadata_.path 20 | if(doubled.__gut_metadata_.subpath != ''): 21 | to_return += str('-', doubled.__gut_metadata_.subpath) 22 | return to_return 23 | 24 | # Creates they key for the returns hash based on the type of object passed in 25 | # obj could be a string of a path to a script with an optional subpath or 26 | # it could be an instance of a doubled object. 27 | func _make_key_from_variant(obj, subpath=null): 28 | var to_return = null 29 | 30 | match typeof(obj): 31 | TYPE_STRING: 32 | # this has to match what is done in _make_key_from_metadata 33 | to_return = obj 34 | if(subpath != null): 35 | to_return += str('-', subpath) 36 | TYPE_OBJECT: 37 | if(_is_instance(obj)): 38 | to_return = _make_key_from_metadata(obj) 39 | else: 40 | to_return = obj.resource_path 41 | return to_return 42 | 43 | func _add_obj_method(obj, method, subpath=null): 44 | var key = _make_key_from_variant(obj, subpath) 45 | if(_is_instance(obj)): 46 | key = obj 47 | 48 | if(!returns.has(key)): 49 | returns[key] = {} 50 | if(!returns[key].has(method)): 51 | returns[key][method] = [] 52 | 53 | return key 54 | 55 | # ############## 56 | # Public 57 | # ############## 58 | 59 | # TODO: This method is only used in tests and should be refactored out. It 60 | # does not support inner classes and isn't helpful. 61 | func set_return(obj, method, value, parameters=null): 62 | var key = _add_obj_method(obj, method) 63 | var sp = _utils.StubParams.new(key, method) 64 | sp.parameters = parameters 65 | sp.return_val = value 66 | returns[key][method].append(sp) 67 | 68 | func add_stub(stub_params): 69 | var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath) 70 | returns[key][stub_params.stub_method].append(stub_params) 71 | 72 | # Gets a stubbed return value for the object and method passed in. If the 73 | # instance was stubbed it will use that, otherwise it will use the path and 74 | # subpath of the object to try to find a value. 75 | # 76 | # It will also use the optional list of parameter values to find a value. If 77 | # the objet was stubbed with no parameters then any parameters will match. 78 | # If it was stubbed with specific paramter values then it will try to match. 79 | # If the parameters do not match BUT there was also an empty paramter list stub 80 | # then it will return those. 81 | # If it cannot find anything that matches then null is returned.for 82 | # 83 | # Parameters 84 | # obj: this should be an instance of a doubled object. 85 | # method: the method called 86 | # paramters: optional array of paramter vales to find a return value for. 87 | func get_return(obj, method, parameters=null): 88 | var key = _make_key_from_variant(obj) 89 | var to_return = null 90 | 91 | if(_is_instance(obj)): 92 | if(returns.has(obj) and returns[obj].has(method)): 93 | key = obj 94 | elif(obj.get('__gut_metadata_')): 95 | key = _make_key_from_metadata(obj) 96 | 97 | if(returns.has(key) and returns[key].has(method)): 98 | var param_idx = -1 99 | var null_idx = -1 100 | 101 | for i in range(returns[key][method].size()): 102 | if(returns[key][method][i].parameters == parameters): 103 | param_idx = i 104 | if(returns[key][method][i].parameters == null): 105 | null_idx = i 106 | 107 | # We have matching parameter values so return the stub value for that 108 | if(param_idx != -1): 109 | to_return = returns[key][method][param_idx].return_val 110 | # We found a case where the parameters were not specified so return 111 | # parameters for that 112 | elif(null_idx != -1): 113 | to_return = returns[key][method][null_idx].return_val 114 | else: 115 | _lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.')) 116 | else: 117 | _lgr.info('Unstubbed call to ' + method) 118 | 119 | 120 | return to_return 121 | 122 | func clear(): 123 | returns.clear() 124 | 125 | func get_logger(): 126 | return _lgr 127 | 128 | func set_logger(logger): 129 | _lgr = logger 130 | 131 | func to_s(): 132 | var text = '' 133 | for thing in returns: 134 | text += str(thing) + "\n" 135 | for method in returns[thing]: 136 | text += str("\t", method, "\n") 137 | for i in range(returns[thing][method].size()): 138 | text += "\t\t" + returns[thing][method][i].to_s() + "\n" 139 | return text 140 | -------------------------------------------------------------------------------- /demo_3d.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=13 format=2] 2 | 3 | [ext_resource path="res://demo_3d.gd" type="Script" id=1] 4 | 5 | [sub_resource type="BoxShape" id=1] 6 | extents = Vector3( 4, 0.1, 4 ) 7 | 8 | [sub_resource type="CubeMesh" id=2] 9 | size = Vector3( 8, 0.2, 8 ) 10 | 11 | [sub_resource type="CylinderMesh" id=3] 12 | top_radius = 0.5 13 | bottom_radius = 0.5 14 | height = 0.1 15 | radial_segments = 6 16 | rings = 1 17 | 18 | [sub_resource type="SpatialMaterial" id=4] 19 | roughness = 0.0 20 | 21 | [sub_resource type="QuadMesh" id=5] 22 | size = Vector2( 2, 1 ) 23 | 24 | [sub_resource type="ViewportTexture" id=6] 25 | viewport_path = NodePath("HexGrid/Highlight/Viewport") 26 | 27 | [sub_resource type="SpatialMaterial" id=7] 28 | resource_local_to_scene = true 29 | flags_transparent = true 30 | albedo_texture = SubResource( 6 ) 31 | 32 | [sub_resource type="BoxShape" id=8] 33 | extents = Vector3( 4, 0.1, 2 ) 34 | 35 | [sub_resource type="CubeMesh" id=9] 36 | size = Vector3( 8, 0.2, 4 ) 37 | 38 | [sub_resource type="ViewportTexture" id=10] 39 | viewport_path = NodePath("HexGrid2/Highlight/Viewport") 40 | 41 | [sub_resource type="SpatialMaterial" id=11] 42 | resource_local_to_scene = true 43 | flags_transparent = true 44 | albedo_texture = SubResource( 10 ) 45 | roughness = 0.0 46 | 47 | [node name="Spatial" type="Spatial"] 48 | 49 | [node name="Camera" type="Camera" parent="."] 50 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 5 ) 51 | 52 | [node name="HexGrid" type="StaticBody" parent="."] 53 | script = ExtResource( 1 ) 54 | 55 | [node name="CollisionShape" type="CollisionShape" parent="HexGrid"] 56 | shape = SubResource( 1 ) 57 | 58 | [node name="MeshInstance" type="MeshInstance" parent="HexGrid"] 59 | mesh = SubResource( 2 ) 60 | material/0 = null 61 | 62 | [node name="Highlight" type="MeshInstance" parent="HexGrid"] 63 | transform = Transform( -1.62921e-07, 0, 1, 0, 1, 0, -1, 0, -1.62921e-07, 0, 0.2, 0 ) 64 | mesh = SubResource( 3 ) 65 | material/0 = SubResource( 4 ) 66 | 67 | [node name="Viewport" type="Viewport" parent="HexGrid/Highlight"] 68 | size = Vector2( 200, 100 ) 69 | transparent_bg = true 70 | hdr = false 71 | usage = 0 72 | render_target_v_flip = true 73 | 74 | [node name="Label" type="Label" parent="HexGrid/Highlight/Viewport"] 75 | margin_right = 41.0 76 | margin_bottom = 14.0 77 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 78 | text = "PLANE" 79 | 80 | [node name="PlaneCoords" type="Label" parent="HexGrid/Highlight/Viewport"] 81 | margin_left = 50.0 82 | margin_right = 97.0 83 | margin_bottom = 14.0 84 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 85 | text = "PLANE" 86 | 87 | [node name="Label2" type="Label" parent="HexGrid/Highlight/Viewport"] 88 | margin_left = 18.0 89 | margin_top = 20.0 90 | margin_right = 58.0 91 | margin_bottom = 34.0 92 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 93 | text = "HEX" 94 | 95 | [node name="HexCoords" type="Label" parent="HexGrid/Highlight/Viewport"] 96 | margin_left = 50.0 97 | margin_top = 20.0 98 | margin_right = 90.0 99 | margin_bottom = 34.0 100 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 101 | text = "HEX" 102 | 103 | [node name="LabelQuad" type="MeshInstance" parent="HexGrid/Highlight"] 104 | transform = Transform( -2.8213e-07, 0, -1, 0, 1, 0, 1, 0, -2.8213e-07, 0, 0.3, 0.5 ) 105 | mesh = SubResource( 5 ) 106 | material/0 = SubResource( 7 ) 107 | 108 | [node name="HexGrid2" type="StaticBody" parent="."] 109 | transform = Transform( 1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 3, -2 ) 110 | script = ExtResource( 1 ) 111 | 112 | [node name="CollisionShape" type="CollisionShape" parent="HexGrid2"] 113 | shape = SubResource( 8 ) 114 | 115 | [node name="MeshInstance" type="MeshInstance" parent="HexGrid2"] 116 | mesh = SubResource( 9 ) 117 | material/0 = null 118 | 119 | [node name="Highlight" type="MeshInstance" parent="HexGrid2"] 120 | transform = Transform( -1.62921e-07, 0, 1, 0, 1, 0, -1, 0, -1.62921e-07, 0, 0.2, 0 ) 121 | mesh = SubResource( 3 ) 122 | material/0 = SubResource( 4 ) 123 | 124 | [node name="Viewport" type="Viewport" parent="HexGrid2/Highlight"] 125 | size = Vector2( 200, 100 ) 126 | transparent_bg = true 127 | hdr = false 128 | usage = 0 129 | render_target_v_flip = true 130 | 131 | [node name="Label" type="Label" parent="HexGrid2/Highlight/Viewport"] 132 | margin_right = 40.0 133 | margin_bottom = 14.0 134 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 135 | text = "PLANE" 136 | 137 | [node name="PlaneCoords" type="Label" parent="HexGrid2/Highlight/Viewport"] 138 | margin_left = 50.0 139 | margin_right = 97.0 140 | margin_bottom = 14.0 141 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 142 | text = "PLANE" 143 | 144 | [node name="Label2" type="Label" parent="HexGrid2/Highlight/Viewport"] 145 | margin_left = 18.0 146 | margin_top = 20.0 147 | margin_right = 58.0 148 | margin_bottom = 34.0 149 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 150 | text = "HEX" 151 | 152 | [node name="HexCoords" type="Label" parent="HexGrid2/Highlight/Viewport"] 153 | margin_left = 50.0 154 | margin_top = 20.0 155 | margin_right = 90.0 156 | margin_bottom = 34.0 157 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 158 | text = "HEX" 159 | 160 | [node name="LabelQuad" type="MeshInstance" parent="HexGrid2/Highlight"] 161 | transform = Transform( -1.62921e-07, 1, 4.37114e-08, 0, -4.37114e-08, 1, 1, 1.62921e-07, 7.1215e-15, 0, 0.3, 0.5 ) 162 | mesh = SubResource( 5 ) 163 | material/0 = SubResource( 11 ) 164 | 165 | [connection signal="input_event" from="HexGrid" to="HexGrid" method="_on_HexGrid_input_event"] 166 | [connection signal="input_event" from="HexGrid2" to="HexGrid2" method="_on_HexGrid_input_event"] 167 | -------------------------------------------------------------------------------- /addons/gut/signal_watcher.gd: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | #The MIT License (MIT) 3 | #===================== 4 | # 5 | #Copyright (c) 2019 Tom "Butch" Wesley 6 | # 7 | #Permission is hereby granted, free of charge, to any person obtaining a copy 8 | #of this software and associated documentation files (the "Software"), to deal 9 | #in the Software without restriction, including without limitation the rights 10 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | #copies of the Software, and to permit persons to whom the Software is 12 | #furnished to do so, subject to the following conditions: 13 | # 14 | #The above copyright notice and this permission notice shall be included in 15 | #all copies or substantial portions of the Software. 16 | # 17 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | #THE SOFTWARE. 24 | # 25 | ################################################################################ 26 | 27 | # Some arbitrary string that should never show up by accident. If it does, then 28 | # shame on you. 29 | const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' 30 | 31 | # This hash holds the objects that are being watched, the signals that are being 32 | # watched, and an array of arrays that contains arguments that were passed 33 | # each time the signal was emitted. 34 | # 35 | # For example: 36 | # _watched_signals => { 37 | # ref1 => { 38 | # 'signal1' => [[], [], []], 39 | # 'signal2' => [[p1, p2]], 40 | # 'signal3' => [[p1]] 41 | # }, 42 | # ref2 => { 43 | # 'some_signal' => [], 44 | # 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]] 45 | # } 46 | # } 47 | # 48 | # In this sample: 49 | # - signal1 on the ref1 object was emitted 3 times and each time, zero 50 | # parameters were passed. 51 | # - signal3 on ref1 was emitted once and passed a single parameter 52 | # - some_signal on ref2 was never emitted. 53 | # - other_signal on ref2 was emitted 3 times, each time with 3 parameters. 54 | var _watched_signals = {} 55 | var _utils = load('res://addons/gut/utils.gd').new() 56 | 57 | func _add_watched_signal(obj, name): 58 | # SHORTCIRCUIT - ignore dupes 59 | if(_watched_signals.has(obj) and _watched_signals[obj].has(name)): 60 | return 61 | 62 | if(!_watched_signals.has(obj)): 63 | _watched_signals[obj] = {name:[]} 64 | else: 65 | _watched_signals[obj][name] = [] 66 | obj.connect(name, self, '_on_watched_signal', [obj, name]) 67 | 68 | # This handles all the signals that are watched. It supports up to 9 parameters 69 | # which could be emitted by the signal and the two parameters used when it is 70 | # connected via watch_signal. I chose 9 since you can only specify up to 9 71 | # parameters when dynamically calling a method via call (per the Godot 72 | # documentation, i.e. some_object.call('some_method', 1, 2, 3...)). 73 | # 74 | # Based on the documentation of emit_signal, it appears you can only pass up 75 | # to 4 parameters when firing a signal. I haven't verified this, but this should 76 | # future proof this some if the value ever grows. 77 | func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \ 78 | arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \ 79 | arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \ 80 | arg10=ARG_NOT_SET, arg11=ARG_NOT_SET): 81 | var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11] 82 | 83 | # strip off any unused vars. 84 | var idx = args.size() -1 85 | while(str(args[idx]) == ARG_NOT_SET): 86 | args.remove(idx) 87 | idx -= 1 88 | 89 | # retrieve object and signal name from the array and remove them. These 90 | # will always be at the end since they are added when the connect happens. 91 | var signal_name = args[args.size() -1] 92 | args.pop_back() 93 | var object = args[args.size() -1] 94 | args.pop_back() 95 | 96 | _watched_signals[object][signal_name].append(args) 97 | 98 | func does_object_have_signal(object, signal_name): 99 | var signals = object.get_signal_list() 100 | for i in range(signals.size()): 101 | if(signals[i]['name'] == signal_name): 102 | return true 103 | return false 104 | 105 | func watch_signals(object): 106 | var signals = object.get_signal_list() 107 | for i in range(signals.size()): 108 | _add_watched_signal(object, signals[i]['name']) 109 | 110 | func watch_signal(object, signal_name): 111 | var did = false 112 | if(does_object_have_signal(object, signal_name)): 113 | _add_watched_signal(object, signal_name) 114 | did = true 115 | return did 116 | 117 | func get_emit_count(object, signal_name): 118 | var to_return = -1 119 | if(is_watching(object, signal_name)): 120 | to_return = _watched_signals[object][signal_name].size() 121 | return to_return 122 | 123 | func did_emit(object, signal_name): 124 | var did = false 125 | if(is_watching(object, signal_name)): 126 | did = get_emit_count(object, signal_name) != 0 127 | return did 128 | 129 | func print_object_signals(object): 130 | var list = object.get_signal_list() 131 | for i in range(list.size()): 132 | print(list[i].name, "\n ", list[i]) 133 | 134 | func get_signal_parameters(object, signal_name, index=-1): 135 | var params = null 136 | if(is_watching(object, signal_name)): 137 | var all_params = _watched_signals[object][signal_name] 138 | if(all_params.size() > 0): 139 | if(index == -1): 140 | index = all_params.size() -1 141 | params = all_params[index] 142 | return params 143 | 144 | func is_watching_object(object): 145 | return _watched_signals.has(object) 146 | 147 | func is_watching(object, signal_name): 148 | return _watched_signals.has(object) and _watched_signals[object].has(signal_name) 149 | 150 | func clear(): 151 | for obj in _watched_signals: 152 | for signal_name in _watched_signals[obj]: 153 | if(_utils.is_not_freed(obj)): 154 | obj.disconnect(signal_name, self, '_on_watched_signal') 155 | _watched_signals.clear() 156 | 157 | # Returns a list of all the signal names that were emitted by the object. 158 | # If the object is not being watched then an empty list is returned. 159 | func get_signals_emitted(obj): 160 | var emitted = [] 161 | if(is_watching_object(obj)): 162 | for signal_name in _watched_signals[obj]: 163 | if(_watched_signals[obj][signal_name].size() > 0): 164 | emitted.append(signal_name) 165 | 166 | return emitted 167 | -------------------------------------------------------------------------------- /addons/gut/method_maker.gd: -------------------------------------------------------------------------------- 1 | # This class will generate method decleration lines based on method meta 2 | # data. It will create defaults that match the method data. 3 | # 4 | # -------------------- 5 | # function meta data 6 | # -------------------- 7 | # name: 8 | # flags: 9 | # args: [{ 10 | # (class_name:), 11 | # (hint:0), 12 | # (hint_string:), 13 | # (name:), 14 | # (type:4), 15 | # (usage:7) 16 | # }] 17 | # default_args [] 18 | 19 | var _utils = load('res://addons/gut/utils.gd').new() 20 | var _lgr = _utils.get_logger() 21 | const PARAM_PREFIX = 'p_' 22 | 23 | # ------------------------------------------------------ 24 | # _supported_defaults 25 | # 26 | # This array contains all the data types that are supported for default values. 27 | # If a value is supported it will contain either an empty string or a prefix 28 | # that should be used when setting the parameter default value. 29 | # For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false) 30 | # but things like Vectors and Colors do since only the parameters to create a 31 | # new Vecotr or Color are included in the metadata. 32 | # ------------------------------------------------------ 33 | # TYPE_NIL = 0 — Variable is of type nil (only applied for null). 34 | # TYPE_BOOL = 1 — Variable is of type bool. 35 | # TYPE_INT = 2 — Variable is of type int. 36 | # TYPE_REAL = 3 — Variable is of type float/real. 37 | # TYPE_STRING = 4 — Variable is of type String. 38 | # TYPE_VECTOR2 = 5 — Variable is of type Vector2. 39 | # TYPE_RECT2 = 6 — Variable is of type Rect2. 40 | # TYPE_VECTOR3 = 7 — Variable is of type Vector3. 41 | # TYPE_COLOR = 14 — Variable is of type Color. 42 | # TYPE_OBJECT = 17 — Variable is of type Object. 43 | # TYPE_DICTIONARY = 18 — Variable is of type Dictionary. 44 | # TYPE_ARRAY = 19 — Variable is of type Array. 45 | # TYPE_VECTOR2_ARRAY = 24 — Variable is of type PoolVector2Array. 46 | 47 | 48 | 49 | # TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D. 50 | # TYPE_PLANE = 9 — Variable is of type Plane. 51 | # TYPE_QUAT = 10 — Variable is of type Quat. 52 | # TYPE_AABB = 11 — Variable is of type AABB. 53 | # TYPE_BASIS = 12 — Variable is of type Basis. 54 | # TYPE_TRANSFORM = 13 — Variable is of type Transform. 55 | # TYPE_NODE_PATH = 15 — Variable is of type NodePath. 56 | # TYPE_RID = 16 — Variable is of type RID. 57 | # TYPE_RAW_ARRAY = 20 — Variable is of type PoolByteArray. 58 | # TYPE_INT_ARRAY = 21 — Variable is of type PoolIntArray. 59 | # TYPE_REAL_ARRAY = 22 — Variable is of type PoolRealArray. 60 | # TYPE_STRING_ARRAY = 23 — Variable is of type PoolStringArray. 61 | # TYPE_VECTOR3_ARRAY = 25 — Variable is of type PoolVector3Array. 62 | # TYPE_COLOR_ARRAY = 26 — Variable is of type PoolColorArray. 63 | # TYPE_MAX = 27 — Marker for end of type constants. 64 | # ------------------------------------------------------ 65 | var _supported_defaults = [] 66 | 67 | func _init(): 68 | for i in range(TYPE_MAX): 69 | _supported_defaults.append(null) 70 | 71 | # These types do not require a prefix for defaults 72 | _supported_defaults[TYPE_NIL] = '' 73 | _supported_defaults[TYPE_BOOL] = '' 74 | _supported_defaults[TYPE_INT] = '' 75 | _supported_defaults[TYPE_REAL] = '' 76 | _supported_defaults[TYPE_OBJECT] = '' 77 | _supported_defaults[TYPE_ARRAY] = '' 78 | _supported_defaults[TYPE_STRING] = '' 79 | _supported_defaults[TYPE_DICTIONARY] = '' 80 | _supported_defaults[TYPE_VECTOR2_ARRAY] = '' 81 | 82 | # These require a prefix for whatever default is provided 83 | _supported_defaults[TYPE_VECTOR2] = 'Vector2' 84 | _supported_defaults[TYPE_RECT2] = 'Rect2' 85 | _supported_defaults[TYPE_VECTOR3] = 'Vector3' 86 | _supported_defaults[TYPE_COLOR] = 'Color' 87 | 88 | # ############### 89 | # Private 90 | # ############### 91 | 92 | func _is_supported_default(type_flag): 93 | return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null 94 | 95 | # Creates a list of paramters with defaults of null unless a default value is 96 | # found in the metadata. If a default is found in the meta then it is used if 97 | # it is one we know how support. 98 | # 99 | # If a default is found that we don't know how to handle then this method will 100 | # return null. 101 | func _get_arg_text(method_meta): 102 | var text = '' 103 | var args = method_meta.args 104 | var defaults = [] 105 | var has_unsupported_defaults = false 106 | 107 | # fill up the defaults with null defaults for everything that doesn't have 108 | # a default in the meta data. default_args is an array of default values 109 | # for the last n parameters where n is the size of default_args so we only 110 | # add nulls for everything up to the first parameter with a default. 111 | for i in range(args.size() - method_meta.default_args.size()): 112 | defaults.append('null') 113 | 114 | # Add meta-data defaults. 115 | for i in range(method_meta.default_args.size()): 116 | var t = args[defaults.size()]['type'] 117 | var value = '' 118 | if(_is_supported_default(t)): 119 | # strings are special, they need quotes around the value 120 | if(t == TYPE_STRING): 121 | value = str("'", str(method_meta.default_args[i]), "'") 122 | # Colors need the parens but things like Vector2 and Rect2 don't 123 | elif(t == TYPE_COLOR): 124 | value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')') 125 | # Everything else puts the prefix (if one is there) fomr _supported_defaults 126 | # in front. The to_lower is used b/c for some reason the defaults for 127 | # null, true, false are all "Null", "True", "False". 128 | else: 129 | value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) 130 | else: 131 | _lgr.warn(str( 132 | 'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i])) 133 | value = str('unsupported=',t) 134 | has_unsupported_defaults = true 135 | 136 | defaults.append(value) 137 | 138 | # construct the string of parameters 139 | for i in range(args.size()): 140 | text += str(PARAM_PREFIX, args[i].name, '=', defaults[i]) 141 | if(i != args.size() -1): 142 | text += ', ' 143 | 144 | # if we don't know how to make a default then we have to return null b/c 145 | # it will cause a runtime error and it's one thing we could return to let 146 | # callers know it didn't work. 147 | if(has_unsupported_defaults): 148 | text = null 149 | 150 | return text 151 | 152 | # ############### 153 | # Public 154 | # ############### 155 | 156 | # Creates a delceration for a function based off of function metadata. All 157 | # types whose defaults are supported will have their values. If a datatype 158 | # is not supported and the paramter has a default, a warning message will be 159 | # printed and the decleration will return null. 160 | func get_decleration_text(meta): 161 | var param_text = _get_arg_text(meta) 162 | var text = null 163 | if(param_text != null): 164 | text = str('func ', meta.name, '(', param_text, '):') 165 | return text 166 | 167 | # creates a call to the function in meta in the super's class. 168 | func get_super_call_text(meta): 169 | var params = '' 170 | var all_supported = true 171 | 172 | for i in range(meta.args.size()): 173 | params += PARAM_PREFIX + meta.args[i].name 174 | if(meta.args.size() > 1 and i != meta.args.size() -1): 175 | params += ', ' 176 | 177 | return str('.', meta.name, '(', params, ')') 178 | 179 | func get_spy_call_parameters_text(meta): 180 | var called_with = 'null' 181 | if(meta.args.size() > 0): 182 | called_with = '[' 183 | for i in range(meta.args.size()): 184 | called_with += str(PARAM_PREFIX, meta.args[i].name) 185 | if(i < meta.args.size() - 1): 186 | called_with += ', ' 187 | called_with += ']' 188 | return called_with 189 | 190 | func get_logger(): 191 | return _lgr 192 | 193 | func set_logger(logger): 194 | _lgr = logger 195 | -------------------------------------------------------------------------------- /addons/gut/test_collector.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Used to keep track of info about each test ran. 3 | # ------------------------------------------------------------------------------ 4 | class Test: 5 | # indicator if it passed or not. defaults to true since it takes only 6 | # one failure to make it not pass. _fail in gut will set this. 7 | var passed = true 8 | # the name of the function 9 | var name = "" 10 | # flag to know if the name has been printed yet. 11 | var has_printed_name = false 12 | # the line number the test is on 13 | var line_number = -1 14 | 15 | # ------------------------------------------------------------------------------ 16 | # ------------------------------------------------------------------------------ 17 | class TestScript: 18 | var inner_class_name = null 19 | var tests = [] 20 | var path = null 21 | var _utils = null 22 | var _lgr = null 23 | 24 | func _init(utils=null, logger=null): 25 | _utils = utils 26 | _lgr = logger 27 | 28 | func to_s(): 29 | var to_return = path 30 | if(inner_class_name != null): 31 | to_return += str('.', inner_class_name) 32 | to_return += "\n" 33 | for i in range(tests.size()): 34 | to_return += str(' ', tests[i].name, "\n") 35 | return to_return 36 | 37 | func get_new(): 38 | var TheScript = load(path) 39 | var inst = null 40 | if(inner_class_name != null): 41 | inst = TheScript.get(inner_class_name).new() 42 | else: 43 | inst = TheScript.new() 44 | return inst 45 | 46 | func get_full_name(): 47 | var to_return = path 48 | if(inner_class_name != null): 49 | to_return += '.' + inner_class_name 50 | return to_return 51 | 52 | func get_filename(): 53 | return path.get_file() 54 | 55 | func has_inner_class(): 56 | return inner_class_name != null 57 | 58 | func export_to(config_file, section): 59 | config_file.set_value(section, 'path', path) 60 | config_file.set_value(section, 'inner_class', inner_class_name) 61 | var names = [] 62 | for i in range(tests.size()): 63 | names.append(tests[i].name) 64 | config_file.set_value(section, 'tests', names) 65 | 66 | func _remap_path(path): 67 | var to_return = path 68 | if(!_utils.file_exists(path)): 69 | _lgr.debug('Checking for remap for: ' + path) 70 | var remap_path = path.get_basename() + '.gd.remap' 71 | if(_utils.file_exists(remap_path)): 72 | var cf = ConfigFile.new() 73 | cf.load(remap_path) 74 | to_return = cf.get_value('remap', 'path') 75 | else: 76 | _lgr.warn('Could not find remap file ' + remap_path) 77 | return to_return 78 | 79 | func import_from(config_file, section): 80 | path = config_file.get_value(section, 'path') 81 | path = _remap_path(path) 82 | var test_names = config_file.get_value(section, 'tests') 83 | for i in range(test_names.size()): 84 | var t = Test.new() 85 | t.name = test_names[i] 86 | tests.append(t) 87 | # Null is an acceptable value, but you can't pass null as a default to 88 | # get_value since it thinks you didn't send a default...then it spits 89 | # out red text. This works around that. 90 | var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') 91 | if(inner_name != 'Placeholder'): 92 | inner_class_name = inner_name 93 | else: # just being explicit 94 | inner_class_name = null 95 | 96 | 97 | # ------------------------------------------------------------------------------ 98 | # start test_collector, I don't think I like the name. 99 | # ------------------------------------------------------------------------------ 100 | var scripts = [] 101 | var _test_prefix = 'test_' 102 | var _test_class_prefix = 'Test' 103 | 104 | var _utils = load('res://addons/gut/utils.gd').new() 105 | var _lgr = _utils.get_logger() 106 | 107 | func _parse_script(script): 108 | var file = File.new() 109 | var line = "" 110 | var line_count = 0 111 | var inner_classes = [] 112 | var scripts_found = [] 113 | 114 | file.open(script.path, 1) 115 | while(!file.eof_reached()): 116 | line_count += 1 117 | line = file.get_line() 118 | #Add a test 119 | if(line.begins_with("func " + _test_prefix)): 120 | var from = line.find(_test_prefix) 121 | var line_len = line.find("(") - from 122 | var new_test = Test.new() 123 | new_test.name = line.substr(from, line_len) 124 | new_test.line_number = line_count 125 | script.tests.append(new_test) 126 | 127 | if(line.begins_with('class ')): 128 | var iclass_name = line.replace('class ', '') 129 | iclass_name = iclass_name.replace(':', '') 130 | if(iclass_name.begins_with(_test_class_prefix)): 131 | inner_classes.append(iclass_name) 132 | 133 | scripts_found.append(script.path) 134 | 135 | for i in range(inner_classes.size()): 136 | var ts = TestScript.new(_utils, _lgr) 137 | ts.path = script.path 138 | ts.inner_class_name = inner_classes[i] 139 | if(_parse_inner_class_tests(ts)): 140 | scripts.append(ts) 141 | scripts_found.append(script.path + '[' + inner_classes[i] +']') 142 | 143 | file.close() 144 | return scripts_found 145 | 146 | func _parse_inner_class_tests(script): 147 | var inst = script.get_new() 148 | 149 | if(!inst is _utils.Test): 150 | _lgr.warn('Ignoring ' + script.inner_class_name + ' because it starts with "' + _test_class_prefix + '" but does not extend addons/gut/test.gd') 151 | return false 152 | 153 | var methods = inst.get_method_list() 154 | for i in range(methods.size()): 155 | var name = methods[i]['name'] 156 | if(name.begins_with(_test_prefix) and methods[i]['flags'] == 65): 157 | var t = Test.new() 158 | t.name = name 159 | script.tests.append(t) 160 | 161 | return true 162 | # ----------------- 163 | # Public 164 | # ----------------- 165 | func add_script(path): 166 | # SHORTCIRCUIT 167 | if(has_script(path)): 168 | return [] 169 | 170 | var f = File.new() 171 | # SHORTCIRCUIT 172 | if(!f.file_exists(path)): 173 | _lgr.error('Could not find script: ' + path) 174 | return 175 | 176 | var ts = TestScript.new(_utils, _lgr) 177 | ts.path = path 178 | scripts.append(ts) 179 | return _parse_script(ts) 180 | 181 | func to_s(): 182 | var to_return = '' 183 | for i in range(scripts.size()): 184 | to_return += scripts[i].to_s() + "\n" 185 | return to_return 186 | func get_logger(): 187 | return _lgr 188 | 189 | func set_logger(logger): 190 | _lgr = logger 191 | 192 | func get_test_prefix(): 193 | return _test_prefix 194 | 195 | func set_test_prefix(test_prefix): 196 | _test_prefix = test_prefix 197 | 198 | func get_test_class_prefix(): 199 | return _test_class_prefix 200 | 201 | func set_test_class_prefix(test_class_prefix): 202 | _test_class_prefix = test_class_prefix 203 | 204 | func clear(): 205 | scripts.clear() 206 | 207 | func has_script(path): 208 | var found = false 209 | var idx = 0 210 | while(idx < scripts.size() and !found): 211 | if(scripts[idx].path == path): 212 | found = true 213 | else: 214 | idx += 1 215 | return found 216 | 217 | func export_tests(path): 218 | if(_utils.is_version_31()): 219 | _lgr.error("Exporting and importing not supported in 3.1 yet. There is a workaround, check the wiki.") 220 | return false 221 | 222 | var success = true 223 | var f = ConfigFile.new() 224 | for i in range(scripts.size()): 225 | scripts[i].export_to(f, str('TestScript-', i)) 226 | var result = f.save(path) 227 | if(result != OK): 228 | _lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result)) 229 | success = false 230 | return success 231 | 232 | func import_tests(path): 233 | if(_utils.is_version_31()): 234 | _lgr.error("Exporting and importing not supported in 3.1 yet. There is a workaround, check the wiki.") 235 | return false 236 | var success = false 237 | var f = ConfigFile.new() 238 | var result = f.load(path) 239 | if(result != OK): 240 | _lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result)) 241 | else: 242 | var sections = f.get_sections() 243 | for key in sections: 244 | var ts = TestScript.new(_utils, _lgr) 245 | ts.import_from(f, key) 246 | scripts.append(ts) 247 | success = true 248 | return success 249 | -------------------------------------------------------------------------------- /HexCell.gd: -------------------------------------------------------------------------------- 1 | """ 2 | A single cell of a hexagonal grid. 3 | 4 | There are many ways to orient a hex grid, this library was written 5 | with the following assumptions: 6 | 7 | * The hexes use a flat-topped orientation; 8 | * Axial coordinates use +x => NE; +y => N; 9 | * Offset coords have odd rows shifted up half a step. 10 | 11 | Using x,y instead of the reference's preferred x,z for axial coords makes 12 | following along with the reference a little more tricky, but is less confusing 13 | when using Godot's Vector2(x, y) objects. 14 | 15 | 16 | ## Usage: 17 | 18 | #### var cube_coords; var axial_coords; var offset_coords 19 | 20 | Cube coordinates are used internally as the canonical representation, but 21 | both axial and offset coordinates can be read and modified through these 22 | properties. 23 | 24 | #### func get_adjacent(direction) 25 | 26 | Returns the neighbouring HexCell in the given direction. 27 | 28 | The direction should be one of the DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, or 29 | DIR_NW constants provided by the HexCell class. 30 | 31 | #### func get_all_adjacent() 32 | 33 | Returns an array of the six HexCell instances neighbouring this one. 34 | 35 | #### func get_all_within(distance) 36 | 37 | Returns an array of all the HexCells within the given number of steps, 38 | including the current hex. 39 | 40 | #### func get_ring(distance) 41 | 42 | Returns an array of all the HexCells at the given distance from the current. 43 | 44 | #### func distance_to(target) 45 | 46 | Returns the number of hops needed to get from this hex to the given target. 47 | 48 | The target can be supplied as either a HexCell instance, cube or axial 49 | coordinates. 50 | 51 | #### func line_to(target) 52 | 53 | Returns an array of all the hexes crossed when drawing a straight line 54 | between this hex and another. 55 | 56 | The target can be supplied as either a HexCell instance, cube or axial 57 | coordinates. 58 | 59 | The items in the array will be in the order of traversal, and include both 60 | the start (current) hex, as well as the final target. 61 | 62 | """ 63 | extends Resource 64 | #warning-ignore-all:unused_class_variable 65 | 66 | # We use unit-size flat-topped hexes 67 | const size = Vector2(1, sqrt(3)/2) 68 | # Directions of neighbouring cells 69 | const DIR_N = Vector3(0, 1, -1) 70 | const DIR_NE = Vector3(1, 0, -1) 71 | const DIR_SE = Vector3(1, -1, 0) 72 | const DIR_S = Vector3(0, -1, 1) 73 | const DIR_SW = Vector3(-1, 0, 1) 74 | const DIR_NW = Vector3(-1, 1, 0) 75 | const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW] 76 | 77 | 78 | # Cube coords are canonical 79 | var cube_coords = Vector3(0, 0, 0) setget set_cube_coords, get_cube_coords 80 | # but other coord systems can be used 81 | var axial_coords setget set_axial_coords, get_axial_coords 82 | var offset_coords setget set_offset_coords, get_offset_coords 83 | 84 | 85 | func _init(coords=null): 86 | # HexCells can be created with coordinates 87 | if coords: 88 | self.cube_coords = obj_to_coords(coords) 89 | 90 | func new_hex(coords): 91 | # Returns a new HexCell instance 92 | return get_script().new(coords) 93 | 94 | """ 95 | Handle coordinate access and conversion 96 | """ 97 | func obj_to_coords(val): 98 | # Returns suitable cube coordinates for the given object 99 | # The given object can an be one of: 100 | # * Vector3 of standard cube coords; 101 | # * Vector2 of axial coords; 102 | # * HexCell instance 103 | # Any other type of value will return null 104 | # 105 | # NB that offset coords are NOT supported, as they are 106 | # indistinguishable from axial coords. 107 | 108 | if typeof(val) == TYPE_VECTOR3: 109 | return val 110 | elif typeof(val) == TYPE_VECTOR2: 111 | return axial_to_cube_coords(val) 112 | elif typeof(val) == TYPE_OBJECT and val.has_method("get_cube_coords"): 113 | return val.get_cube_coords() 114 | # Fall through to nothing 115 | return 116 | 117 | func axial_to_cube_coords(val): 118 | # Returns the Vector3 cube coordinates for an axial Vector2 119 | var x = val.x 120 | var y = val.y 121 | return Vector3(x, y, -x - y) 122 | 123 | func round_coords(val): 124 | # Rounds floaty coordinate to the nearest whole number cube coords 125 | if typeof(val) == TYPE_VECTOR2: 126 | val = axial_to_cube_coords(val) 127 | 128 | # Straight round them 129 | var rounded = Vector3(round(val.x), round(val.y), round(val.z)) 130 | 131 | # But recalculate the one with the largest diff so that x+y+z=0 132 | var diffs = (rounded - val).abs() 133 | if diffs.x > diffs.y and diffs.x > diffs.z: 134 | rounded.x = -rounded.y - rounded.z 135 | elif diffs.y > diffs.z: 136 | rounded.y = -rounded.x - rounded.z 137 | else: 138 | rounded.z = -rounded.x - rounded.y 139 | 140 | return rounded 141 | 142 | 143 | func get_cube_coords(): 144 | # Returns a Vector3 of the cube coordinates 145 | return cube_coords 146 | 147 | func set_cube_coords(val): 148 | # Sets the position from a Vector3 of cube coordinates 149 | if abs(val.x + val.y + val.z) > 0.0001: 150 | print("WARNING: Invalid cube coordinates for hex (x+y+z!=0): ", val) 151 | return 152 | cube_coords = round_coords(val) 153 | 154 | func get_axial_coords(): 155 | # Returns a Vector2 of the axial coordinates 156 | return Vector2(cube_coords.x, cube_coords.y) 157 | 158 | func set_axial_coords(val): 159 | # Sets position from a Vector2 of axial coordinates 160 | set_cube_coords(axial_to_cube_coords(val)) 161 | 162 | func get_offset_coords(): 163 | # Returns a Vector2 of the offset coordinates 164 | var x = int(cube_coords.x) 165 | var y = int(cube_coords.y) 166 | var off_y = y + (x - (x & 1)) / 2 167 | return Vector2(x, off_y) 168 | 169 | func set_offset_coords(val): 170 | # Sets position from a Vector2 of offset coordinates 171 | var x = int(val.x) 172 | var y = int(val.y) 173 | var cube_y = y - (x - (x & 1)) / 2 174 | self.set_axial_coords(Vector2(x, cube_y)) 175 | 176 | 177 | """ 178 | Finding our neighbours 179 | """ 180 | func get_adjacent(dir): 181 | # Returns a HexCell instance for the given direction from this. 182 | # Intended for one of the DIR_* consts, but really any Vector2 or x+y+z==0 Vector3 will do. 183 | if typeof(dir) == TYPE_VECTOR2: 184 | dir = axial_to_cube_coords(dir) 185 | return new_hex(self.cube_coords + dir) 186 | 187 | func get_all_adjacent(): 188 | # Returns an array of HexCell instances representing adjacent locations 189 | var cells = Array() 190 | for coord in DIR_ALL: 191 | cells.append(new_hex(self.cube_coords + coord)) 192 | return cells 193 | 194 | func get_all_within(distance): 195 | # Returns an array of all HexCell instances within the given distance 196 | var cells = Array() 197 | for dx in range(-distance, distance+1): 198 | for dy in range(max(-distance, -distance - dx), min(distance, distance - dx) + 1): 199 | cells.append(new_hex(self.axial_coords + Vector2(dx, dy))) 200 | return cells 201 | 202 | func get_ring(distance): 203 | # Returns an array of all HexCell instances at the given distance 204 | if distance < 1: 205 | return [new_hex(self.cube_coords)] 206 | # Start at the top (+y) and walk in a clockwise circle 207 | var cells = Array() 208 | var current = new_hex(self.cube_coords + (DIR_N * distance)) 209 | for dir in [DIR_SE, DIR_S, DIR_SW, DIR_NW, DIR_N, DIR_NE]: 210 | for _step in range(distance): 211 | cells.append(current) 212 | current = current.get_adjacent(dir) 213 | return cells 214 | 215 | func distance_to(target): 216 | # Returns the number of hops from this hex to another 217 | # Can be passed cube or axial coords, or another HexCell instance 218 | target = obj_to_coords(target) 219 | return int(( 220 | abs(cube_coords.x - target.x) 221 | + abs(cube_coords.y - target.y) 222 | + abs(cube_coords.z - target.z) 223 | ) / 2) 224 | 225 | func line_to(target): 226 | # Returns an array of HexCell instances representing 227 | # a straight path from here to the target, including both ends 228 | target = obj_to_coords(target) 229 | # End of our lerp is nudged so it never lands exactly on an edge 230 | var nudged_target = target + Vector3(1e-6, 2e-6, -3e-6) 231 | var steps = distance_to(target) 232 | var path = [] 233 | for dist in range(steps): 234 | var lerped = cube_coords.linear_interpolate(nudged_target, float(dist) / steps) 235 | path.append(new_hex(round_coords(lerped))) 236 | path.append(new_hex(target)) 237 | return path 238 | 239 | -------------------------------------------------------------------------------- /test/unit/test_hexcell.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | class TestNew: 4 | extends "res://addons/gut/test.gd" 5 | 6 | var HexCell = load("res://HexCell.gd") 7 | var cell 8 | 9 | func setup(): 10 | cell = null 11 | 12 | 13 | func test_null(): 14 | cell = HexCell.new() 15 | assert_eq(cell.axial_coords, Vector2(0, 0)) 16 | 17 | func test_cube(): 18 | cell = HexCell.new(Vector3(1, 1, -2)) 19 | assert_eq(cell.axial_coords, Vector2(1, 1)) 20 | 21 | func test_axial(): 22 | cell = HexCell.new(Vector2(1, -1)) 23 | assert_eq(cell.axial_coords, Vector2(1, -1)) 24 | 25 | func test_instance(): 26 | var test_cell = HexCell.new(Vector3(-1, 2, -1)) 27 | cell = HexCell.new(test_cell) 28 | assert_eq(cell.axial_coords, Vector2(-1, 2)) 29 | 30 | 31 | class TestConversions: 32 | extends "res://addons/gut/test.gd" 33 | 34 | var HexCell = load("res://HexCell.gd") 35 | var cell 36 | 37 | func setup(): 38 | cell = HexCell.new() 39 | 40 | 41 | func test_axial_to_cube(): 42 | assert_eq(cell.axial_to_cube_coords(Vector2(2, 1)), Vector3(2, 1, -3)) 43 | assert_eq(cell.axial_to_cube_coords(Vector2(-1, -1)), Vector3(-1, -1, 2)) 44 | 45 | func test_rounding(): 46 | assert_eq(cell.round_coords(Vector3(0.1, 0.5, -0.6)), Vector3(0, 1, -1)) 47 | assert_eq(cell.round_coords(Vector3(-0.4, -1.3, 1.7)), Vector3(-1, -1, 2)) 48 | 49 | assert_eq(cell.round_coords(Vector2(-0.1, 0.6)), Vector3(0, 1, -1)) 50 | assert_eq(cell.round_coords(Vector2(4.2, -5.5)), Vector3(4, -5, 1)) 51 | 52 | 53 | class TestCoords: 54 | extends "res://addons/gut/test.gd" 55 | 56 | var HexCell = load("res://HexCell.gd") 57 | var cell 58 | 59 | func setup(): 60 | cell = HexCell.new() 61 | 62 | 63 | func test_from_cubic_positive(): 64 | cell.cube_coords = Vector3(2, 1, -3) 65 | assert_eq(cell.cube_coords, Vector3(2, 1, -3)) 66 | assert_eq(cell.axial_coords, Vector2(2, 1)) 67 | assert_eq(cell.offset_coords, Vector2(2, 2)) 68 | func test_from_cubic_negative(): 69 | cell.cube_coords = Vector3(-1, -1, 2) 70 | assert_eq(cell.cube_coords, Vector3(-1, -1, 2)) 71 | assert_eq(cell.axial_coords, Vector2(-1, -1)) 72 | assert_eq(cell.offset_coords, Vector2(-1, -2)) 73 | func test_from_cubic_invalid(): 74 | cell.cube_coords = Vector3(1, 2, 3) 75 | assert_eq(cell.cube_coords, Vector3(0, 0, 0)) 76 | 77 | func test_from_axial_positive(): 78 | cell.axial_coords = Vector2(2, 1) 79 | assert_eq(cell.cube_coords, Vector3(2, 1, -3)) 80 | assert_eq(cell.axial_coords, Vector2(2, 1)) 81 | assert_eq(cell.offset_coords, Vector2(2, 2)) 82 | func test_from_axial_negative(): 83 | cell.axial_coords = Vector2(-1, -1) 84 | assert_eq(cell.cube_coords, Vector3(-1, -1, 2)) 85 | assert_eq(cell.axial_coords, Vector2(-1, -1)) 86 | assert_eq(cell.offset_coords, Vector2(-1, -2)) 87 | 88 | func test_from_offset_positive(): 89 | cell.offset_coords = Vector2(2, 2) 90 | assert_eq(cell.cube_coords, Vector3(2, 1, -3)) 91 | assert_eq(cell.axial_coords, Vector2(2, 1)) 92 | assert_eq(cell.offset_coords, Vector2(2, 2)) 93 | func test_from_offset_negative(): 94 | cell.offset_coords = Vector2(-1, -2) 95 | assert_eq(cell.cube_coords, Vector3(-1, -1, 2)) 96 | assert_eq(cell.axial_coords, Vector2(-1, -1)) 97 | assert_eq(cell.offset_coords, Vector2(-1, -2)) 98 | 99 | 100 | class TestNearby: 101 | extends "res://addons/gut/test.gd" 102 | 103 | var HexCell = load("res://HexCell.gd") 104 | var cell 105 | 106 | func setup(): 107 | cell = HexCell.new(Vector2(1, 2)) 108 | 109 | func check_expected(cells, expected): 110 | # Check that a bunch of cells are what were expected 111 | assert_eq(cells.size(), expected.size()) 112 | for hex in cells: 113 | assert_has(expected, hex.axial_coords) 114 | 115 | 116 | func test_adjacent(): 117 | var foo = cell.get_adjacent(HexCell.DIR_N) 118 | assert_eq(foo.axial_coords, Vector2(1, 3)) 119 | foo = cell.get_adjacent(HexCell.DIR_NE) 120 | assert_eq(foo.axial_coords, Vector2(2, 2)) 121 | foo = cell.get_adjacent(HexCell.DIR_SE) 122 | assert_eq(foo.axial_coords, Vector2(2, 1)) 123 | foo = cell.get_adjacent(HexCell.DIR_S) 124 | assert_eq(foo.axial_coords, Vector2(1, 1)) 125 | foo = cell.get_adjacent(HexCell.DIR_SW) 126 | assert_eq(foo.axial_coords, Vector2(0, 2)) 127 | foo = cell.get_adjacent(HexCell.DIR_NW) 128 | assert_eq(foo.axial_coords, Vector2(0, 3)) 129 | func test_not_really_adjacent(): 130 | var foo = cell.get_adjacent(Vector3(-3, -3, 6)) 131 | assert_eq(foo.axial_coords, Vector2(-2, -1)) 132 | func test_adjacent_axial(): 133 | var foo = cell.get_adjacent(Vector2(1, 1)) 134 | assert_eq(foo.axial_coords, Vector2(2, 3)) 135 | 136 | func test_all_adjacent(): 137 | var coords = [] 138 | for foo in cell.get_all_adjacent(): 139 | coords.append(foo.axial_coords) 140 | assert_has(coords, Vector2(1, 3)) 141 | assert_has(coords, Vector2(2, 2)) 142 | assert_has(coords, Vector2(2, 1)) 143 | assert_has(coords, Vector2(1, 1)) 144 | assert_has(coords, Vector2(0, 2)) 145 | assert_has(coords, Vector2(0, 3)) 146 | 147 | func test_all_within_0(): 148 | var expected = [ 149 | Vector2(1, 2), 150 | ] 151 | var cells = cell.get_all_within(0) 152 | check_expected(cells, expected) 153 | func test_all_within_1(): 154 | var expected = [ 155 | Vector2(1, 2), 156 | Vector2(1, 3), 157 | Vector2(2, 2), 158 | Vector2(2, 1), 159 | Vector2(1, 1), 160 | Vector2(0, 2), 161 | Vector2(0, 3), 162 | ] 163 | var cells = cell.get_all_within(1) 164 | check_expected(cells, expected) 165 | func test_all_within_2(): 166 | var expected = [ 167 | Vector2(-1, 4), Vector2(0, 4), Vector2(1, 4), 168 | Vector2(-1, 3), Vector2(0, 3), Vector2(1, 3), Vector2(2, 3), 169 | Vector2(-1, 2), Vector2(0, 2), 170 | Vector2(1, 2), 171 | Vector2(2, 2), Vector2(3, 2), 172 | Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(3, 1), 173 | Vector2(1, 0), Vector2(2, 0), Vector2(3, 0), 174 | ] 175 | var cells = cell.get_all_within(2) 176 | check_expected(cells, expected) 177 | 178 | func test_ring_0(): 179 | var expected = [ 180 | Vector2(1, 2), 181 | ] 182 | var cells = cell.get_ring(0) 183 | check_expected(cells, expected) 184 | func test_ring_1(): 185 | var expected = [ 186 | Vector2(1, 3), 187 | Vector2(2, 2), 188 | Vector2(2, 1), 189 | Vector2(1, 1), 190 | Vector2(0, 2), 191 | Vector2(0, 3), 192 | ] 193 | var cells = cell.get_ring(1) 194 | check_expected(cells, expected) 195 | func test_ring_2(): 196 | var expected = [ 197 | Vector2(1, 4), # Start at +2y 198 | Vector2(2, 3), Vector2(3, 2), # SE 199 | Vector2(3, 1), Vector2(3, 0), # S 200 | Vector2(2, 0), Vector2(1, 0), # SW 201 | Vector2(0, 1), Vector2(-1, 2), # NW 202 | Vector2(-1, 3), Vector2(-1, 4), # N 203 | Vector2(0, 4), # NE 204 | ] 205 | var cells = cell.get_ring(2) 206 | check_expected(cells, expected) 207 | 208 | 209 | class TestBetweenTwo: 210 | extends "res://addons/gut/test.gd" 211 | 212 | var HexCell = load("res://HexCell.gd") 213 | var cell 214 | 215 | func setup(): 216 | cell = HexCell.new(Vector2(1, 2)) 217 | 218 | func test_distance(): 219 | assert_eq(cell.distance_to(Vector2(0, 0)), 3) 220 | assert_eq(cell.distance_to(Vector2(3, 4)), 4) 221 | assert_eq(cell.distance_to(Vector2(-1, -1)), 5) 222 | 223 | func test_line_straight(): 224 | # Straight line, nice and simple 225 | var expected = [ 226 | Vector2(1, 2), 227 | Vector2(2, 2), 228 | Vector2(3, 2), 229 | Vector2(4, 2), 230 | Vector2(5, 2), 231 | ] 232 | var path = cell.line_to(Vector2(5, 2)) 233 | assert_eq(path.size(), expected.size()) 234 | for idx in range(expected.size()): 235 | assert_eq(path[idx].axial_coords, expected[idx]) 236 | 237 | func test_line_angled(): 238 | # It's gone all wibbly-wobbly 239 | var expected = [ 240 | Vector2(1, 2), 241 | Vector2(2, 2), 242 | Vector2(2, 3), 243 | Vector2(3, 3), 244 | Vector2(4, 3), 245 | Vector2(4, 4), 246 | Vector2(5, 4), 247 | ] 248 | var path = cell.line_to(Vector2(5, 4)) 249 | assert_eq(path.size(), expected.size()) 250 | for idx in range(expected.size()): 251 | assert_eq(path[idx].axial_coords, expected[idx]) 252 | 253 | func test_line_edge(): 254 | # Living on the edge between two hexes 255 | var expected = [ 256 | Vector2(1, 2), 257 | Vector2(1, 3), 258 | Vector2(2, 3), 259 | Vector2(2, 4), 260 | Vector2(3, 4), 261 | ] 262 | var path = cell.line_to(Vector2(3, 4)) 263 | assert_eq(path.size(), expected.size()) 264 | for idx in range(expected.size()): 265 | assert_eq(path[idx].axial_coords, expected[idx]) 266 | 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDHexGrid 2 | 3 | Tools for using hexagonal grids in GDScript. 4 | 5 | The reference used for creating this was the amazing guide: 6 | https://www.redblobgames.com/grids/hexagons/ 7 | 8 | Copyright 2018 Mel Collins. 9 | Distributed under the MIT license (see LICENSE.txt). 10 | 11 | ## Orientation 12 | 13 | There are many ways to orient a hex grid, this library was written 14 | using the following assumptions: 15 | 16 | * The hexes use a flat-topped orientation; 17 | * Axial coordinates use +x => NE; +y => N; 18 | * Offset coords have odd rows shifted up half a step; 19 | * Projections of the hex grid into Godot-space use +x => E, +y => S. 20 | 21 | Using x,y instead of the reference's preferred x,z for axial coords makes 22 | following along with the reference a little more tricky, but is less confusing 23 | when using Godot's Vector2(x, y) objects. 24 | 25 | We map hex coordinates to Godot-space with +y flipped to be the down vector 26 | so that it maps neatly to both Godot's 2D coordinate system, and also to 27 | x,z planes in 3D space. 28 | 29 | 30 | ## Usage 31 | 32 | ### HexGrid 33 | 34 | HexGrid is used when you want to position hexes in a 2D or 3D scene. 35 | It translates coordinates between the hex grid and conventional spaces. 36 | 37 | #### var hex_scale = Vector2(...) 38 | 39 | If you want your hexes to display larger than the default 1 x 0.866 units, 40 | then you can customise the scale of the hexes using this property. 41 | 42 | #### func get_hex_center(hex) 43 | 44 | Returns the Godot-space coordinate of the center of the given hex coordinates. 45 | 46 | The coordinates can be given as either a HexCell instance; a Vector3 cube 47 | coordinate, or a Vector2 axial coordinate. 48 | 49 | #### func get_hex_center3(hex [, y]) 50 | 51 | Returns the Godot-space Vector3 of the center of the given hex. 52 | 53 | The coordinates can be given as either a HexCell instance; a Vector3 cube 54 | coordinate, or a Vector2 axial coordinate. 55 | 56 | If a second parameter is given, it will be used for the y value in the 57 | returned Vector3. Otherwise, the y value will be 0. 58 | 59 | #### func get_hex_at(coords) 60 | 61 | Returns HexCell whose grid position contains the given Godot-space coordinates. 62 | 63 | The given value can either be a Vector2 on the grid's plane 64 | or a Vector3, in which case its (x, z) coordinates will be used. 65 | 66 | 67 | ### HexGrid pathfinding 68 | 69 | HexGrid also includes an implementation of the A* pathfinding algorithm. 70 | The class can be used to populate an internal representation of a game grid 71 | with obstacles to traverse. 72 | 73 | This was written with the aid of another amazing guide: 74 | https://www.redblobgames.com/pathfinding/a-star/introduction.html 75 | 76 | #### func set_bounds(min_coords, max_coords) 77 | 78 | Sets the hard outer limits of the path-finding grid. 79 | 80 | The coordinates given are the min and max corners *inside* a bounding 81 | square (diamond in hex visualisation) region. Any hex outside that area 82 | is considered an impassable obstacle. 83 | 84 | The default bounds consider only the origin to be inside, so you're probably 85 | going to want to do something about that. 86 | 87 | #### func get_obstacles() 88 | 89 | Returns a dict of all obstacles and their costs 90 | 91 | The keys are Vector2s of the axial coordinates, the values will be the 92 | cost value. Zero cost means an impassable obstacle. 93 | 94 | #### func add_obstacles(coords, cost=0) 95 | 96 | Adds one or more obstacles to the path-finding grid 97 | 98 | The given coordinates (axial or cube), HexCell instance, or array thereof, 99 | will be added as path-finding obstacles with the given cost. A zero cost 100 | indicates an impassable obstacle. 101 | 102 | #### func remove_obstacles(coords) 103 | 104 | Removes one or more obstacles from the path-finding grid 105 | 106 | The given coordinates (axial or cube), HexCell instance, or array thereof, 107 | will be removed as obstacles from the path-finding grid. 108 | 109 | #### func get_barriers() 110 | 111 | Returns a dict of all barriers in the grid. 112 | 113 | A barrier is an edge of a hex which is either impassable, or has a 114 | non-zero cost to traverse. If two adjacent hexes both have barriers on 115 | their shared edge, their costs are summed. 116 | Barrier costs are in addition to the obstacle (or default) cost of 117 | moving to a hex. 118 | 119 | The outer dict is a mapping of axial coords to an inner barrier dict. 120 | The inner dict maps between HexCell.DIR_* directions and the cost of 121 | travel in that direction. A cost of zero indicates an impassable barrier. 122 | 123 | #### func add_barriers(coords, dirs, cost=0) 124 | 125 | Adds one or more barriers to locations on the grid. 126 | 127 | The given coordinates (axial or cube), HexCell instance, or array thereof, 128 | will have path-finding barriers added in the given HexCell.DIR_* directions 129 | with the given cost. A zero cost indicates an impassable obstacle. 130 | 131 | Existing barriers at given coordinates will not be removed, but will be 132 | overridden if the direction is specified. 133 | 134 | #### func remove_barriers(coords, dirs=null) 135 | 136 | Remove one or more barriers from the path-finding grid. 137 | 138 | The given coordinates (axial or cube), HexCell instance, or array thereof, 139 | will have the path-finding barriers in the supplied HexCell.DIR_* directions 140 | removed. If no direction is specified, all barriers for the given 141 | coordinates will be removed. 142 | 143 | #### func get_hex_cost(coords) 144 | 145 | Returns the cost of moving into the specified grid position. 146 | 147 | Will return 0 if the given grid position is inaccessible. 148 | 149 | #### func get_move_cost(coords, direction) 150 | 151 | Returns the cost of moving from one hex to an adjacent one. 152 | 153 | This method takes into account any barriers defined between the two 154 | hexes, as well as the cost of the target hex. 155 | Will return 0 if the target hex is inaccessible, or if there is an 156 | impassable barrier between the hexes. 157 | 158 | The direction should be provided as one of the HexCell.DIR_* values. 159 | 160 | #### func find_path(start, goal, exceptions=[]) 161 | 162 | Calculates an A* path from the start to the goal. 163 | 164 | Returns a list of HexCell instances charting the path from the given start 165 | coordinates to the goal, including both ends of the journey. 166 | 167 | Exceptions can be specified as the third parameter, and will act as 168 | impassable obstacles for the purposes of this call of the function. 169 | This can be used for pathing around obstacles which may change position 170 | (eg. enemy playing pieces), without having to update the grid's list of 171 | obstacles every time something moves. 172 | 173 | If the goal is an impassable location, the path will terminate at the nearest 174 | adjacent coordinate. In this instance, the goal hex will not be included in 175 | the returned array. 176 | 177 | If there is no path possible to the goal, or any hex adjacent to it, an 178 | empty array is returned. But the algorithm will only know that once it's 179 | visited every tile it can reach, so try not to path to the impossible. 180 | 181 | 182 | ### HexCell 183 | 184 | A HexCell represents a single hex in the grid, and is the meat of the library. 185 | 186 | #### var cube_coords; var axial_coords; var offset_coords 187 | 188 | Cube coordinates are used internally as the canonical representation, but 189 | both axial and offset coordinates can be read and modified through these 190 | properties. 191 | 192 | #### func get_adjacent(direction) 193 | 194 | Returns the neighbouring HexCell in the given direction. 195 | 196 | The direction should be one of the DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, or 197 | DIR_NW constants provided by the HexCell class. 198 | 199 | #### func get_all_adjacent() 200 | 201 | Returns an array of the six HexCell instances neighbouring this one. 202 | 203 | #### func get_all_within(distance) 204 | 205 | Returns an array of all the HexCells within the given number of steps, 206 | including the current hex. 207 | 208 | #### func get_ring(distance) 209 | 210 | Returns an array of all the HexCells at the given distance from the current. 211 | 212 | #### func distance_to(target) 213 | 214 | Returns the number of hops needed to get from this hex to the given target. 215 | 216 | The target can be supplied as either a HexCell instance, cube or axial 217 | coordinates. 218 | 219 | #### func line_to(target) 220 | 221 | Returns an array of all the hexes crossed when drawing a straight line 222 | between this hex and another. 223 | 224 | The target can be supplied as either a HexCell instance, cube or axial 225 | coordinates. 226 | 227 | The items in the array will be in the order of traversal, and include both 228 | the start (current) hex, as well as the final target. 229 | 230 | -------------------------------------------------------------------------------- /addons/gut/optparse.gd: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | #(G)odot (U)nit (T)est class 3 | # 4 | ################################################################################ 5 | #The MIT License (MIT) 6 | #===================== 7 | # 8 | #Copyright (c) 2019 Tom "Butch" Wesley 9 | # 10 | #Permission is hereby granted, free of charge, to any person obtaining a copy 11 | #of this software and associated documentation files (the "Software"), to deal 12 | #in the Software without restriction, including without limitation the rights 13 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | #copies of the Software, and to permit persons to whom the Software is 15 | #furnished to do so, subject to the following conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be included in 18 | #all copies or substantial portions of the Software. 19 | # 20 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | #THE SOFTWARE. 27 | # 28 | ################################################################################ 29 | # Description 30 | # ----------- 31 | # Command line interface for the GUT unit testing tool. Allows you to run tests 32 | # from the command line instead of running a scene. Place this script along with 33 | # gut.gd into your scripts directory at the root of your project. Once there you 34 | # can run this script (from the root of your project) using the following command: 35 | # godot -s -d test/gut/gut_cmdln.gd 36 | # 37 | # See the readme for a list of options and examples. You can also use the -gh 38 | # option to get more information about how to use the command line interface. 39 | # 40 | # Version 6.7.0 41 | ################################################################################ 42 | 43 | #------------------------------------------------------------------------------- 44 | # Parses the command line arguments supplied into an array that can then be 45 | # examined and parsed based on how the gut options work. 46 | #------------------------------------------------------------------------------- 47 | class CmdLineParser: 48 | var _used_options = [] 49 | var _opts = [] 50 | 51 | func _init(): 52 | for i in range(OS.get_cmdline_args().size()): 53 | _opts.append(OS.get_cmdline_args()[i]) 54 | 55 | # Parse out multiple comma delimited values from a command line 56 | # option. Values are separated from option name with "=" and 57 | # additional values are comma separated. 58 | func _parse_array_value(full_option): 59 | var value = _parse_option_value(full_option) 60 | var split = value.split(',') 61 | return split 62 | 63 | # Parse out the value of an option. Values are separated from 64 | # the option name with "=" 65 | func _parse_option_value(full_option): 66 | var split = full_option.split('=') 67 | 68 | if(split.size() > 1): 69 | return split[1] 70 | else: 71 | return null 72 | 73 | # Search _opts for an element that starts with the option name 74 | # specified. 75 | func find_option(name): 76 | var found = false 77 | var idx = 0 78 | 79 | while(idx < _opts.size() and !found): 80 | if(_opts[idx].find(name) == 0): 81 | found = true 82 | else: 83 | idx += 1 84 | 85 | if(found): 86 | return idx 87 | else: 88 | return -1 89 | 90 | func get_array_value(option): 91 | _used_options.append(option) 92 | var to_return = [] 93 | var opt_loc = find_option(option) 94 | if(opt_loc != -1): 95 | to_return = _parse_array_value(_opts[opt_loc]) 96 | _opts.remove(opt_loc) 97 | 98 | return to_return 99 | 100 | # returns the value of an option if it was specfied, null otherwise. This 101 | # used to return the default but that became problemnatic when trying to 102 | # punch through the different places where values could be specified. 103 | func get_value(option): 104 | _used_options.append(option) 105 | var to_return = null 106 | var opt_loc = find_option(option) 107 | if(opt_loc != -1): 108 | to_return = _parse_option_value(_opts[opt_loc]) 109 | _opts.remove(opt_loc) 110 | 111 | return to_return 112 | 113 | # returns true if it finds the option, false if not. 114 | func was_specified(option): 115 | _used_options.append(option) 116 | return find_option(option) != -1 117 | 118 | # Returns any unused command line options. I found that only the -s and 119 | # script name come through from godot, all other options that godot uses 120 | # are not sent through OS.get_cmdline_args(). 121 | # 122 | # This is a onetime thing b/c i kill all items in _used_options 123 | func get_unused_options(): 124 | var to_return = [] 125 | for i in range(_opts.size()): 126 | to_return.append(_opts[i].split('=')[0]) 127 | 128 | var script_option = to_return.find('-s') 129 | to_return.remove(script_option + 1) 130 | to_return.remove(script_option) 131 | 132 | while(_used_options.size() > 0): 133 | var index = to_return.find(_used_options[0].split("=")[0]) 134 | if(index != -1): 135 | to_return.remove(index) 136 | _used_options.remove(0) 137 | 138 | return to_return 139 | 140 | #------------------------------------------------------------------------------- 141 | # Simple class to hold a command line option 142 | #------------------------------------------------------------------------------- 143 | class Option: 144 | var value = null 145 | var option_name = '' 146 | var default = null 147 | var description = '' 148 | 149 | func _init(name, default_value, desc=''): 150 | option_name = name 151 | default = default_value 152 | description = desc 153 | value = null#default_value 154 | 155 | func pad(value, size, pad_with=' '): 156 | var to_return = value 157 | for i in range(value.length(), size): 158 | to_return += pad_with 159 | 160 | return to_return 161 | 162 | func to_s(min_space=0): 163 | var subbed_desc = description 164 | if(subbed_desc.find('[default]') != -1): 165 | subbed_desc = subbed_desc.replace('[default]', str(default)) 166 | return pad(option_name, min_space) + subbed_desc 167 | 168 | #------------------------------------------------------------------------------- 169 | # The high level interface between this script and the command line options 170 | # supplied. Uses Option class and CmdLineParser to extract information from 171 | # the command line and make it easily accessible. 172 | #------------------------------------------------------------------------------- 173 | var options = [] 174 | var _opts = [] 175 | var _banner = '' 176 | 177 | func add(name, default, desc): 178 | options.append(Option.new(name, default, desc)) 179 | 180 | func get_value(name): 181 | var found = false 182 | var idx = 0 183 | 184 | while(idx < options.size() and !found): 185 | if(options[idx].option_name == name): 186 | found = true 187 | else: 188 | idx += 1 189 | 190 | if(found): 191 | return options[idx].value 192 | else: 193 | print("COULD NOT FIND OPTION " + name) 194 | return null 195 | 196 | func set_banner(banner): 197 | _banner = banner 198 | 199 | func print_help(): 200 | var longest = 0 201 | for i in range(options.size()): 202 | if(options[i].option_name.length() > longest): 203 | longest = options[i].option_name.length() 204 | 205 | print('---------------------------------------------------------') 206 | print(_banner) 207 | 208 | print("\nOptions\n-------") 209 | for i in range(options.size()): 210 | print(' ' + options[i].to_s(longest + 2)) 211 | print('---------------------------------------------------------') 212 | 213 | func print_options(): 214 | for i in range(options.size()): 215 | print(options[i].option_name + '=' + str(options[i].value)) 216 | 217 | func parse(): 218 | var parser = CmdLineParser.new() 219 | 220 | for i in range(options.size()): 221 | var t = typeof(options[i].default) 222 | # only set values that were specified at the command line so that 223 | # we can punch through default and config values correctly later. 224 | # Without this check, you can't tell the difference between the 225 | # defaults and what was specified, so you can't punch through 226 | # higher level options. 227 | if(parser.was_specified(options[i].option_name)): 228 | if(t == TYPE_INT): 229 | options[i].value = int(parser.get_value(options[i].option_name)) 230 | elif(t == TYPE_STRING): 231 | options[i].value = parser.get_value(options[i].option_name) 232 | elif(t == TYPE_ARRAY): 233 | options[i].value = parser.get_array_value(options[i].option_name) 234 | elif(t == TYPE_BOOL): 235 | options[i].value = parser.was_specified(options[i].option_name) 236 | elif(t == TYPE_NIL): 237 | print(options[i].option_name + ' cannot be processed, it has a nil datatype') 238 | else: 239 | print(options[i].option_name + ' cannot be processsed, it has unknown datatype:' + str(t)) 240 | 241 | var unused = parser.get_unused_options() 242 | if(unused.size() > 0): 243 | print("Unrecognized options: ", unused) 244 | return false 245 | 246 | return true 247 | -------------------------------------------------------------------------------- /addons/gut/GutScene.gd: -------------------------------------------------------------------------------- 1 | extends Panel 2 | 3 | onready var _script_list = $ScriptsList 4 | onready var _nav = { 5 | prev = $Navigation/Previous, 6 | next = $Navigation/Next, 7 | run = $Navigation/Run, 8 | current_script = $Navigation/CurrentScript, 9 | show_scripts = $Navigation/ShowScripts 10 | } 11 | onready var _progress = { 12 | script = $ScriptProgress, 13 | test = $TestProgress 14 | } 15 | onready var _summary = { 16 | failing = $Summary/Failing, 17 | passing = $Summary/Passing 18 | } 19 | 20 | onready var _extras = $ExtraOptions 21 | onready var _ignore_pauses = $ExtraOptions/IgnorePause 22 | onready var _continue_button = $Continue/Continue 23 | onready var _text_box = $TextDisplay/RichTextLabel 24 | 25 | onready var _titlebar = { 26 | bar = $TitleBar, 27 | time = $TitleBar/Time, 28 | label = $TitleBar/Title 29 | } 30 | 31 | var _mouse = { 32 | down = false, 33 | in_title = false, 34 | down_pos = null, 35 | in_handle = false 36 | } 37 | var _is_running = false 38 | var _time = 0 39 | const DEFAULT_TITLE = 'Gut: The Godot Unit Testing tool.' 40 | var _utils = load('res://addons/gut/utils.gd').new() 41 | var _text_box_blocker_enabled = true 42 | var _pre_maximize_size = null 43 | 44 | signal end_pause 45 | signal ignore_pause 46 | signal log_level_changed 47 | signal run_script 48 | signal run_single_script 49 | signal script_selected 50 | 51 | func _ready(): 52 | _pre_maximize_size = rect_size 53 | _hide_scripts() 54 | _update_controls() 55 | _nav.current_script.set_text("No scripts available") 56 | set_title() 57 | clear_summary() 58 | $TitleBar/Time.set_text("") 59 | $ExtraOptions/DisableBlocker.pressed = !_text_box_blocker_enabled 60 | _extras.visible = false 61 | update() 62 | 63 | func _process(delta): 64 | if(_is_running): 65 | _time += delta 66 | var disp_time = round(_time * 100)/100 67 | $TitleBar/Time.set_text(str(disp_time)) 68 | 69 | func _draw(): # needs get_size() 70 | # Draw the lines in the corner to show where you can 71 | # drag to resize the dialog 72 | var grab_margin = 3 73 | var line_space = 3 74 | var grab_line_color = Color(.4, .4, .4) 75 | for i in range(1, 10): 76 | var x = rect_size - Vector2(i * line_space, grab_margin) 77 | var y = rect_size - Vector2(grab_margin, i * line_space) 78 | draw_line(x, y, grab_line_color, 1, true) 79 | 80 | func _on_Maximize_draw(): 81 | # draw the maximize square thing. 82 | var btn = $TitleBar/Maximize 83 | btn.set_text('') 84 | var w = btn.get_size().x 85 | var h = btn.get_size().y 86 | btn.draw_rect(Rect2(0, 0, w, h), Color(0, 0, 0, 1)) 87 | btn.draw_rect(Rect2(2, 4, w - 4, h - 6), Color(1,1,1,1)) 88 | 89 | func _on_ShowExtras_draw(): 90 | var btn = $Continue/ShowExtras 91 | btn.set_text('') 92 | var start_x = 20 93 | var start_y = 15 94 | var pad = 5 95 | var color = Color(.1, .1, .1, 1) 96 | var width = 2 97 | for i in range(3): 98 | var y = start_y + pad * i 99 | btn.draw_line(Vector2(start_x, y), Vector2(btn.get_size().x - start_x, y), color, width, true) 100 | 101 | # #################### 102 | # GUI Events 103 | # #################### 104 | func _on_Run_pressed(): 105 | _run_mode() 106 | emit_signal('run_script', get_selected_index()) 107 | 108 | func _on_CurrentScript_pressed(): 109 | _run_mode() 110 | emit_signal('run_single_script', get_selected_index()) 111 | 112 | func _on_Previous_pressed(): 113 | _select_script(get_selected_index() - 1) 114 | 115 | func _on_Next_pressed(): 116 | _select_script(get_selected_index() + 1) 117 | 118 | func _on_LogLevelSlider_value_changed(value): 119 | emit_signal('log_level_changed', $LogLevelSlider.value) 120 | 121 | func _on_Continue_pressed(): 122 | _continue_button.disabled = true 123 | emit_signal('end_pause') 124 | 125 | func _on_IgnorePause_pressed(): 126 | var checked = _ignore_pauses.is_pressed() 127 | emit_signal('ignore_pause', checked) 128 | if(checked): 129 | emit_signal('end_pause') 130 | _continue_button.disabled = true 131 | 132 | func _on_ShowScripts_pressed(): 133 | _toggle_scripts() 134 | 135 | func _on_ScriptsList_item_selected(index): 136 | _select_script(index) 137 | 138 | func _on_TitleBar_mouse_entered(): 139 | _mouse.in_title = true 140 | 141 | func _on_TitleBar_mouse_exited(): 142 | _mouse.in_title = false 143 | 144 | func _input(event): 145 | if(event is InputEventMouseButton): 146 | if(event.button_index == 1): 147 | _mouse.down = event.pressed 148 | if(_mouse.down): 149 | _mouse.down_pos = event.position 150 | 151 | if(_mouse.in_title): 152 | if(event is InputEventMouseMotion and _mouse.down): 153 | set_position(get_position() + (event.position - _mouse.down_pos)) 154 | _mouse.down_pos = event.position 155 | 156 | if(_mouse.in_handle): 157 | if(event is InputEventMouseMotion and _mouse.down): 158 | var new_size = rect_size + event.position - _mouse.down_pos 159 | var new_mouse_down_pos = event.position 160 | rect_size = new_size 161 | _mouse.down_pos = new_mouse_down_pos 162 | _pre_maximize_size = rect_size 163 | 164 | func _on_ResizeHandle_mouse_entered(): 165 | _mouse.in_handle = true 166 | 167 | func _on_ResizeHandle_mouse_exited(): 168 | _mouse.in_handle = false 169 | 170 | # Send scroll type events through to the text box 171 | func _on_FocusBlocker_gui_input(ev): 172 | if(_text_box_blocker_enabled): 173 | if(ev is InputEventPanGesture): 174 | get_text_box()._gui_input(ev) 175 | # convert a drag into a pan gesture so it scrolls. 176 | elif(ev is InputEventScreenDrag): 177 | var converted = InputEventPanGesture.new() 178 | converted.delta = Vector2(0, ev.relative.y) 179 | converted.position = Vector2(0, 0) 180 | get_text_box()._gui_input(converted) 181 | elif(ev is InputEventMouseButton and (ev.button_index == BUTTON_WHEEL_DOWN or ev.button_index == BUTTON_WHEEL_UP)): 182 | get_text_box()._gui_input(ev) 183 | else: 184 | get_text_box()._gui_input(ev) 185 | print(ev) 186 | 187 | func _on_RichTextLabel_gui_input(ev): 188 | pass 189 | # leaving this b/c it is wired up and might have to send 190 | # more signals through 191 | print(ev) 192 | 193 | func _on_Copy_pressed(): 194 | _text_box.select_all() 195 | _text_box.copy() 196 | _text_box.deselect() 197 | 198 | func _on_DisableBlocker_toggled(button_pressed): 199 | _text_box_blocker_enabled = !button_pressed 200 | 201 | func _on_ShowExtras_toggled(button_pressed): 202 | _extras.visible = button_pressed 203 | 204 | func _on_Maximize_pressed(): 205 | if(rect_size == _pre_maximize_size): 206 | maximize() 207 | else: 208 | rect_size = _pre_maximize_size 209 | # #################### 210 | # Private 211 | # #################### 212 | func _run_mode(is_running=true): 213 | if(is_running): 214 | _time = 0 215 | _summary.failing.set_text("0") 216 | _summary.passing.set_text("0") 217 | _is_running = is_running 218 | 219 | _hide_scripts() 220 | var ctrls = $Navigation.get_children() 221 | for i in range(ctrls.size()): 222 | ctrls[i].disabled = is_running 223 | 224 | func _select_script(index): 225 | $Navigation/CurrentScript.set_text(_script_list.get_item_text(index)) 226 | _script_list.select(index) 227 | _update_controls() 228 | 229 | func _toggle_scripts(): 230 | if(_script_list.visible): 231 | _hide_scripts() 232 | else: 233 | _show_scripts() 234 | 235 | func _show_scripts(): 236 | _script_list.show() 237 | 238 | func _hide_scripts(): 239 | _script_list.hide() 240 | 241 | func _update_controls(): 242 | var is_empty = _script_list.get_selected_items().size() == 0 243 | if(is_empty): 244 | _nav.next.disabled = true 245 | _nav.prev.disabled = true 246 | else: 247 | var index = get_selected_index() 248 | _nav.prev.disabled = index <= 0 249 | _nav.next.disabled = index >= _script_list.get_item_count() - 1 250 | 251 | _nav.run.disabled = is_empty 252 | _nav.current_script.disabled = is_empty 253 | _nav.show_scripts.disabled = is_empty 254 | 255 | 256 | # #################### 257 | # Public 258 | # #################### 259 | func run_mode(is_running=true): 260 | _run_mode(is_running) 261 | 262 | func set_scripts(scripts): 263 | _script_list.clear() 264 | for i in range(scripts.size()): 265 | _script_list.add_item(scripts[i]) 266 | _select_script(0) 267 | _update_controls() 268 | 269 | func select_script(index): 270 | _select_script(index) 271 | 272 | func get_selected_index(): 273 | return _script_list.get_selected_items()[0] 274 | 275 | func get_log_level(): 276 | return $LogLevelSlider.value 277 | 278 | func set_log_level(value): 279 | $LogLevelSlider.value = _utils.nvl(value, 0) 280 | 281 | func set_ignore_pause(should): 282 | _ignore_pauses.pressed = should 283 | 284 | func get_ignore_pause(): 285 | return _ignore_pauses.pressed 286 | 287 | func get_text_box(): 288 | return $TextDisplay/RichTextLabel 289 | 290 | func end_run(): 291 | _run_mode(false) 292 | _update_controls() 293 | 294 | func set_progress_script_max(value): 295 | _progress.script.set_max(value) 296 | 297 | func set_progress_script_value(value): 298 | _progress.script.set_value(value) 299 | 300 | func set_progress_test_max(value): 301 | _progress.test.set_max(value) 302 | 303 | func set_progress_test_value(value): 304 | _progress.test.set_value(value) 305 | 306 | func clear_progress(): 307 | _progress.test.set_value(0) 308 | _progress.script.set_value(0) 309 | 310 | func pause(): 311 | print('we got here') 312 | _continue_button.disabled = false 313 | 314 | func set_title(title=null): 315 | if(title == null): 316 | $TitleBar/Title.set_text(DEFAULT_TITLE) 317 | else: 318 | $TitleBar/Title.set_text(title) 319 | 320 | func get_run_duration(): 321 | return $TitleBar/Time.text.to_float() 322 | 323 | func add_passing(amount=1): 324 | if(!_summary): 325 | return 326 | _summary.passing.set_text(str(_summary.passing.get_text().to_int() + amount)) 327 | $Summary.show() 328 | 329 | func add_failing(amount=1): 330 | if(!_summary): 331 | return 332 | _summary.failing.set_text(str(_summary.failing.get_text().to_int() + amount)) 333 | $Summary.show() 334 | 335 | func clear_summary(): 336 | _summary.passing.set_text("0") 337 | _summary.failing.set_text("0") 338 | $Summary.hide() 339 | 340 | func maximize(): 341 | if(is_inside_tree()): 342 | var vp_size_offset = get_viewport().size 343 | rect_size = vp_size_offset / get_scale() 344 | -------------------------------------------------------------------------------- /test/unit/test_pathfinding.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/gut/test.gd" 2 | 3 | var HexCell = load("res://HexCell.gd") 4 | var HexGrid = load("res://HexGrid.gd") 5 | var grid 6 | var map 7 | # This is the hex map we'll test with: 8 | # remember: +y is N, +x is NE 9 | """ 10 | . 11 | . 12 | . . 13 | . . 14 | . . . 15 | . . . 16 | . . . . 17 | . O B . 18 | OF O . C 19 | . E O . 20 | O O D 21 | . O . 22 | . . 23 | . A 24 | G 25 | . <- (0, 0) 26 | """ 27 | var a_pos = Vector2(2, 0) 28 | var b_pos = Vector2(4, 2) 29 | var c_pos = Vector2(7, 0) 30 | var d_pos = Vector2(5, 0) 31 | var e_pos = Vector2(2, 2) 32 | var f_pos = Vector2(1, 3) 33 | var g_pos = Vector2(1, 0) 34 | var obstacles = [ 35 | Vector2(2, 1), 36 | Vector2(3, 1), 37 | Vector2(4, 1), 38 | Vector2(1, 2), 39 | Vector2(3, 2), 40 | Vector2(1, 3), 41 | Vector2(2, 3), 42 | ] 43 | 44 | func setup(): 45 | grid = HexGrid.new() 46 | grid.set_bounds(Vector2(0, 0), Vector2(7, 4)) 47 | grid.add_obstacles(obstacles) 48 | 49 | func test_bounds(): 50 | # Push the boundaries 51 | # Check that the test boundary works properly 52 | assert_eq(grid.get_hex_cost(Vector2(0, 0)), grid.path_cost_default, "SW is open") 53 | assert_eq(grid.get_hex_cost(Vector2(0, 4)), grid.path_cost_default, "W is open") 54 | assert_eq(grid.get_hex_cost(Vector2(7, 0)), grid.path_cost_default, "E is open") 55 | assert_eq(grid.get_hex_cost(Vector2(7, 4)), grid.path_cost_default, "NE is open") 56 | assert_eq(grid.get_hex_cost(Vector2(8, 2)), 0, "Too much X is blocked") 57 | assert_eq(grid.get_hex_cost(Vector2(6, 5)), 0, "Too much Y is blocked") 58 | assert_eq(grid.get_hex_cost(Vector2(-1, 2)), 0, "Too little X is blocked") 59 | assert_eq(grid.get_hex_cost(Vector2(6, -1)), 0, "Too little Y is blocked") 60 | func test_negative_bounds(): 61 | # Test negative space 62 | grid = HexGrid.new() 63 | grid.set_bounds(Vector2(-5, -5), Vector2(-2, -2)) 64 | assert_eq(grid.get_hex_cost(Vector2(-2, -2)), grid.path_cost_default) 65 | assert_eq(grid.get_hex_cost(Vector2(-5, -5)), grid.path_cost_default) 66 | assert_eq(grid.get_hex_cost(Vector2(0, 0)), 0) 67 | assert_eq(grid.get_hex_cost(Vector2(-6, -3)), 0) 68 | assert_eq(grid.get_hex_cost(Vector2(-3, -1)), 0) 69 | func test_roundabounds(): 70 | # We can also go both ways 71 | grid.set_bounds(Vector2(-3, -3), Vector2(2, 2)) 72 | assert_eq(grid.get_hex_cost(Vector2(-3, -3)), grid.path_cost_default) 73 | assert_eq(grid.get_hex_cost(Vector2(2, 2)), grid.path_cost_default) 74 | assert_eq(grid.get_hex_cost(Vector2(0, 0)), grid.path_cost_default) 75 | assert_eq(grid.get_hex_cost(Vector2(-4, 0)), 0) 76 | assert_eq(grid.get_hex_cost(Vector2(0, 3)), 0) 77 | 78 | func test_grid_obstacles(): 79 | # Make sure we can obstacleize the grid 80 | assert_eq(grid.get_obstacles().size(), obstacles.size()) 81 | # Test adding via a HexCell instance 82 | grid.add_obstacles(HexCell.new(Vector2(0, 0))) 83 | assert_eq(grid.get_obstacles()[Vector2(0, 0)], 0) 84 | # Test replacing an obstacle 85 | grid.add_obstacles(Vector2(0, 0), 2) 86 | assert_eq(grid.get_obstacles()[Vector2(0, 0)], 2) 87 | # Test removing an obstacle 88 | grid.remove_obstacles(Vector2(0, 0)) 89 | assert_does_not_have(grid.get_obstacles(), Vector2(0, 0)) 90 | # Make sure removing a non-obstacle doesn't error 91 | grid.remove_obstacles(Vector2(0, 0)) 92 | 93 | func test_grid_barriers(): 94 | # Make sure we can barrier things on the grid 95 | assert_eq(grid.get_barriers().size(), 0) 96 | # Add a barrier 97 | var coords = Vector2(0, 0) 98 | var barriers = grid.get_barriers() 99 | grid.add_barriers(coords, HexCell.DIR_N) 100 | assert_eq(barriers.size(), 1) 101 | assert_has(barriers, coords) 102 | assert_eq(barriers[coords].size(), 1) 103 | assert_has(barriers[coords], HexCell.DIR_N) 104 | # Overwrite the barrier 105 | grid.add_barriers(coords, HexCell.DIR_N, 1337) 106 | assert_eq(barriers[coords][HexCell.DIR_N], 1337) 107 | # Add more barrier to the hex 108 | grid.add_barriers(coords, [HexCell.DIR_S, HexCell.DIR_NE]) 109 | assert_eq(barriers[coords].size(), 3) 110 | assert_has(barriers[coords], HexCell.DIR_N) 111 | # Remove part of the hex's barrier 112 | grid.remove_barriers(coords, [HexCell.DIR_N]) 113 | assert_eq(barriers[coords].size(), 2) 114 | assert_does_not_have(barriers[coords], HexCell.DIR_N) 115 | assert_has(barriers[coords], HexCell.DIR_S) 116 | assert_has(barriers[coords], HexCell.DIR_NE) 117 | # Remove all the hex's barriers 118 | grid.remove_barriers(coords) 119 | assert_eq(barriers.size(), 0) 120 | # Remove no barrier with no error 121 | grid.remove_barriers([Vector2(1, 1), Vector2(2, 2)]) 122 | 123 | 124 | func test_hex_costs(): 125 | # Test that the price is right 126 | assert_eq(grid.get_hex_cost(HexCell.new(Vector2(1, 1))), grid.path_cost_default, "Open hex is open") 127 | assert_eq(grid.get_hex_cost(Vector3(2, 1, -3)), 0, "Obstacle being obstructive") 128 | # Test partial obstacle 129 | grid.add_obstacles(Vector2(1, 1), 1.337) 130 | assert_eq(grid.get_hex_cost(Vector2(1, 1)), 1.337, "9") 131 | 132 | func test_move_costs(): 133 | # Test that more than just hex costs are at work 134 | assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), grid.path_cost_default) 135 | func test_move_cost_barrier(): 136 | # Put up a barrier 137 | grid.add_barriers(Vector2(0, 0), HexCell.DIR_N) 138 | assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 0) 139 | func test_move_cost_barrier_backside(): 140 | # The destination has a barrier 141 | grid.add_barriers(Vector2(0, 1), HexCell.DIR_S) 142 | assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 0) 143 | func test_move_cost_cumulative(): 144 | # Test that moving adds up hex and barrier values 145 | # But NOT from the *starting* hex! 146 | grid.add_obstacles(Vector2(0, 0), 1) 147 | grid.add_obstacles(Vector2(0, 1), 2) 148 | grid.add_barriers(Vector2(0, 0), HexCell.DIR_N, 4) 149 | grid.add_barriers(Vector2(0, 1), HexCell.DIR_S, 8) 150 | assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 14) 151 | 152 | 153 | func check_path(got, expected): 154 | # Assert that the gotten path was the expected route 155 | assert_eq(got.size(), expected.size(), "Path should be as long as expected") 156 | for idx in range(min(got.size(), expected.size())): 157 | var hex = got[idx] 158 | var check = expected[idx] 159 | if typeof(check) == TYPE_ARRAY: 160 | # In case of multiple valid paths 161 | assert_has(check, hex.axial_coords) 162 | else: 163 | assert_eq(check, hex.axial_coords) 164 | 165 | func test_straight_line(): 166 | # Path between A and C is straight 167 | var path = [ 168 | a_pos, 169 | Vector2(3, 0), 170 | Vector2(4, 0), 171 | Vector2(5, 0), 172 | Vector2(6, 0), 173 | c_pos, 174 | ] 175 | check_path(grid.find_path(a_pos, c_pos), path) 176 | 177 | func test_wonky_line(): 178 | # Path between B and C is a bit wonky 179 | var path = [ 180 | b_pos, 181 | [Vector2(5, 1), Vector2(5, 2)], 182 | [Vector2(6, 0), Vector2(6, 1)], 183 | c_pos, 184 | ] 185 | check_path(grid.find_path(HexCell.new(b_pos), HexCell.new(c_pos)), path) 186 | 187 | func test_obstacle(): 188 | # Path between A and B should go around the bottom 189 | var path = [ 190 | a_pos, 191 | Vector2(3, 0), 192 | Vector2(4, 0), 193 | Vector2(5, 0), 194 | Vector2(5, 1), 195 | b_pos, 196 | ] 197 | check_path(grid.find_path(a_pos, b_pos), path) 198 | 199 | func test_walls(): 200 | # Test that we can't walk through walls 201 | var walls = [ 202 | HexCell.DIR_N, 203 | HexCell.DIR_NE, 204 | HexCell.DIR_SE, 205 | HexCell.DIR_S, 206 | # DIR_SE is the only opening 207 | HexCell.DIR_NW, 208 | ] 209 | grid.add_barriers(g_pos, walls) 210 | var path = [ 211 | a_pos, 212 | Vector2(1, 1), 213 | Vector2(0, 1), 214 | Vector2(0, 0), 215 | g_pos, 216 | ] 217 | check_path(grid.find_path(a_pos, g_pos), path) 218 | 219 | func test_slopes(): 220 | # Test that we *can* walk through *some* walls 221 | # A barrier which is passable, but not worth our hex 222 | grid.add_barriers(g_pos, HexCell.DIR_NE, 3) 223 | # A barrier which is marginally better than moving that extra hex 224 | grid.add_barriers(g_pos, HexCell.DIR_N, grid.path_cost_default - 0.1) 225 | var path = [ 226 | a_pos, 227 | Vector2(1, 1), 228 | g_pos, 229 | ] 230 | check_path(grid.find_path(a_pos, g_pos), path) 231 | 232 | func test_rough_terrain(): 233 | # Path between A and B depends on the toughness of D 234 | var short_path = [ 235 | a_pos, 236 | Vector2(3, 0), 237 | Vector2(4, 0), 238 | d_pos, 239 | Vector2(5, 1), 240 | b_pos, 241 | ] 242 | var long_path = [ 243 | a_pos, 244 | Vector2(1, 1), 245 | Vector2(0, 2), 246 | Vector2(0, 3), 247 | Vector2(0, 4), 248 | Vector2(1, 4), 249 | Vector2(2, 4), 250 | Vector2(3, 3), 251 | b_pos, 252 | ] 253 | # The long path is 9 long, the short 6, 254 | # so it should take the long path once d_pos costs more than 3 over default 255 | var tests = { 256 | grid.path_cost_default: short_path, 257 | grid.path_cost_default + 1: short_path, 258 | grid.path_cost_default + 2.9: short_path, 259 | grid.path_cost_default + 3.1: long_path, 260 | grid.path_cost_default + 50: long_path, 261 | 0: long_path, 262 | } 263 | for cost in tests: 264 | grid.add_obstacles(d_pos, cost) 265 | check_path(grid.find_path(a_pos, b_pos), tests[cost]) 266 | 267 | func test_exception(): 268 | # D is impassable, so path between A and B should go around the top as well 269 | var path = [ 270 | a_pos, 271 | Vector2(1, 1), 272 | Vector2(0, 2), 273 | Vector2(0, 3), 274 | Vector2(0, 4), 275 | Vector2(1, 4), 276 | Vector2(2, 4), 277 | Vector2(3, 3), 278 | b_pos, 279 | ] 280 | check_path(grid.find_path(a_pos, b_pos, [d_pos]), path) 281 | func test_exception_hex(): 282 | # Same as the above, but providing an exceptional HexCell instance 283 | var path = [ 284 | a_pos, 285 | Vector2(1, 1), 286 | Vector2(0, 2), 287 | Vector2(0, 3), 288 | Vector2(0, 4), 289 | Vector2(1, 4), 290 | Vector2(2, 4), 291 | Vector2(3, 3), 292 | b_pos, 293 | ] 294 | check_path(grid.find_path(a_pos, b_pos, [HexCell.new(d_pos)]), path) 295 | 296 | func test_exceptional_goal(): 297 | # If D is impassable, we should path to its neighbour 298 | var path = [ 299 | a_pos, 300 | Vector2(3, 0), 301 | Vector2(4, 0), 302 | ] 303 | check_path(grid.find_path(a_pos, d_pos, [d_pos]), path) 304 | 305 | func test_inaccessible(): 306 | # E is inaccessible! 307 | var path = grid.find_path(a_pos, e_pos) 308 | assert_eq(path.size(), 0) 309 | 310 | func test_obstacle_neighbour(): 311 | # Sometimes we can't get to something, but we can get next to it. 312 | var path = [ 313 | a_pos, 314 | Vector2(1, 1), 315 | Vector2(0, 2), 316 | Vector2(0, 3), 317 | ] 318 | check_path(grid.find_path(a_pos, f_pos), path) 319 | 320 | func test_difficult_goal(): 321 | # We should be able to path to a goal, no matter how difficult the final step 322 | grid.add_obstacles(f_pos, 1337) 323 | var path = [ 324 | a_pos, 325 | Vector2(1, 1), 326 | Vector2(0, 2), 327 | Vector2(0, 3), 328 | f_pos, 329 | ] 330 | check_path(grid.find_path(a_pos, f_pos), path) 331 | 332 | -------------------------------------------------------------------------------- /addons/gut/doubler.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Utility class to hold the local and built in methods seperately. Add all local 3 | # methods FIRST, then add built ins. 4 | # ------------------------------------------------------------------------------ 5 | class ScriptMethods: 6 | # List of methods that should not be overloaded when they are not defined 7 | # in the class being doubled. These either break things if they are 8 | # overloaded or do not have a "super" equivalent so we can't just pass 9 | # through. 10 | var _blacklist = [ 11 | 'has_method', 12 | 'get_script', 13 | 'get', 14 | '_notification', 15 | 'get_path', 16 | '_enter_tree', 17 | '_exit_tree', 18 | '_process', 19 | '_draw', 20 | '_physics_process', 21 | '_input', 22 | '_unhandled_input', 23 | '_unhandled_key_input', 24 | '_set', 25 | '_get', # probably 26 | 'emit_signal', # can't handle extra parameters to be sent with signal. 27 | ] 28 | 29 | var built_ins = [] 30 | var local_methods = [] 31 | var _method_names = [] 32 | 33 | func is_blacklisted(method_meta): 34 | return _blacklist.find(method_meta.name) != -1 35 | 36 | func _add_name_if_does_not_have(method_name): 37 | var should_add = _method_names.find(method_name) == -1 38 | if(should_add): 39 | _method_names.append(method_name) 40 | return should_add 41 | 42 | func add_built_in_method(method_meta): 43 | var did_add = _add_name_if_does_not_have(method_meta.name) 44 | if(did_add and !is_blacklisted(method_meta)): 45 | built_ins.append(method_meta) 46 | 47 | func add_local_method(method_meta): 48 | var did_add = _add_name_if_does_not_have(method_meta.name) 49 | if(did_add): 50 | local_methods.append(method_meta) 51 | 52 | func to_s(): 53 | var text = "Locals\n" 54 | for i in range(local_methods.size()): 55 | text += str(" ", local_methods[i].name, "\n") 56 | text += "Built-Ins\n" 57 | for i in range(built_ins.size()): 58 | text += str(" ", built_ins[i].name, "\n") 59 | return text 60 | 61 | # ------------------------------------------------------------------------------ 62 | # Helper class to deal with objects and inner classes. 63 | # ------------------------------------------------------------------------------ 64 | class ObjectInfo: 65 | var _path = null 66 | var _subpaths = [] 67 | var _utils = load('res://addons/gut/utils.gd').new() 68 | 69 | func _init(path, subpath=null): 70 | _path = path 71 | if(subpath != null): 72 | _subpaths = _utils.split_string(subpath, '/') 73 | 74 | # Returns an instance of the class/inner class 75 | func instantiate(): 76 | return get_loaded_class().new() 77 | 78 | # Can't call it get_class because that is reserved so it gets this ugly name. 79 | # Loads up the class and then any inner classes to give back a reference to 80 | # the desired Inner class (if there is any) 81 | func get_loaded_class(): 82 | var LoadedClass = load(_path) 83 | for i in range(_subpaths.size()): 84 | LoadedClass = LoadedClass.get(_subpaths[i]) 85 | return LoadedClass 86 | 87 | func to_s(): 88 | return str(_path, '[', get_subpath(), ']') 89 | 90 | func get_path(): 91 | return _path 92 | 93 | func get_subpath(): 94 | return _utils.join_array(_subpaths, '/') 95 | 96 | func has_subpath(): 97 | return _subpaths.size() != 0 98 | 99 | func get_extends_text(): 100 | var extend = str("extends '", get_path(), '\'') 101 | if(has_subpath()): 102 | extend += str('.', get_subpath().replace('/', '.')) 103 | return extend 104 | 105 | 106 | # ------------------------------------------------------------------------------ 107 | # START Doubler 108 | # ------------------------------------------------------------------------------ 109 | var _output_dir = null 110 | var _stubber = null 111 | var _double_count = 0 # used in making files names unique 112 | var _use_unique_names = true 113 | var _spy = null 114 | 115 | var _utils = load('res://addons/gut/utils.gd').new() 116 | var _lgr = _utils.get_logger() 117 | var _method_maker = _utils.MethodMaker.new() 118 | var _strategy = null 119 | var _swapped_out_strategy = null 120 | 121 | func _temp_strategy(strat): 122 | _swapped_out_strategy = _strategy 123 | _strategy = strat 124 | 125 | func _restore_strategy(): 126 | _strategy = _swapped_out_strategy 127 | 128 | 129 | func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL): 130 | # make sure _method_maker gets logger too 131 | set_logger(_utils.get_logger()) 132 | _strategy = strategy 133 | 134 | # ############### 135 | # Private 136 | # ############### 137 | func _get_indented_line(indents, text): 138 | var to_return = '' 139 | for i in range(indents): 140 | to_return += "\t" 141 | return str(to_return, text, "\n") 142 | 143 | func _write_file(obj_info, dest_path, override_path=null): 144 | var script_methods = _get_methods(obj_info) 145 | 146 | var metadata = _get_stubber_metadata_text(obj_info) 147 | if(override_path): 148 | metadata = _get_stubber_metadata_text(obj_info, override_path) 149 | 150 | var f = File.new() 151 | f.open(dest_path, f.WRITE) 152 | 153 | 154 | f.store_string(str(obj_info.get_extends_text(), "\n")) 155 | f.store_string(metadata) 156 | for i in range(script_methods.local_methods.size()): 157 | f.store_string(_get_func_text(script_methods.local_methods[i])) 158 | for i in range(script_methods.built_ins.size()): 159 | f.store_string(_get_super_func_text(script_methods.built_ins[i])) 160 | f.close() 161 | 162 | func _double_scene_and_script(target_path, dest_path): 163 | var dir = Directory.new() 164 | dir.copy(target_path, dest_path) 165 | 166 | var inst = load(target_path).instance() 167 | var script_path = null 168 | if(inst.get_script()): 169 | script_path = inst.get_script().get_path() 170 | inst.free() 171 | 172 | if(script_path): 173 | var oi = ObjectInfo.new(script_path) 174 | var double_path = _double(oi, target_path) 175 | var dq = '"' 176 | var f = File.new() 177 | f.open(dest_path, f.READ) 178 | var source = f.get_as_text() 179 | f.close() 180 | 181 | source = source.replace(dq + script_path + dq, dq + double_path + dq) 182 | 183 | f.open(dest_path, f.WRITE) 184 | f.store_string(source) 185 | f.close() 186 | 187 | return script_path 188 | 189 | func _get_methods(object_info): 190 | var obj = object_info.instantiate() 191 | # any mehtod in the script or super script 192 | var script_methods = ScriptMethods.new() 193 | var methods = obj.get_method_list() 194 | 195 | # first pass is for local mehtods only 196 | for i in range(methods.size()): 197 | # 65 is a magic number for methods in script, though documentation 198 | # says 64. This picks up local overloads of base class methods too. 199 | if(methods[i].flags == 65): 200 | script_methods.add_local_method(methods[i]) 201 | 202 | 203 | if(_strategy == _utils.DOUBLE_STRATEGY.FULL): 204 | if(_utils.is_version_30()): 205 | # second pass is for anything not local 206 | for i in range(methods.size()): 207 | # 65 is a magic number for methods in script, though documentation 208 | # says 64. This picks up local overloads of base class methods too. 209 | if(methods[i].flags != 65): 210 | script_methods.add_built_in_method(methods[i]) 211 | else: 212 | _lgr.warn('Full doubling is disabled in 3.1') 213 | 214 | return script_methods 215 | 216 | func _get_inst_id_ref_str(inst): 217 | var ref_str = 'null' 218 | if(inst): 219 | ref_str = str('instance_from_id(', inst.get_instance_id(),')') 220 | return ref_str 221 | 222 | func _get_stubber_metadata_text(obj_info, override_path = null): 223 | var path = obj_info.get_path() 224 | if(override_path != null): 225 | path = override_path 226 | return "var __gut_metadata_ = {\n" + \ 227 | "\tpath='" + path + "',\n" + \ 228 | "\tsubpath='" + obj_info.get_subpath() + "',\n" + \ 229 | "\tstubber=" + _get_inst_id_ref_str(_stubber) + ",\n" + \ 230 | "\tspy=" + _get_inst_id_ref_str(_spy) + "\n" + \ 231 | "}\n" 232 | 233 | func _get_spy_text(method_hash): 234 | var txt = '' 235 | if(_spy): 236 | var called_with = _method_maker.get_spy_call_parameters_text(method_hash) 237 | txt += "\t__gut_metadata_.spy.add_call(self, '" + method_hash.name + "', " + called_with + ")\n" 238 | return txt 239 | 240 | func _get_func_text(method_hash): 241 | var ftxt = _method_maker.get_decleration_text(method_hash) + "\n" 242 | 243 | var called_with = _method_maker.get_spy_call_parameters_text(method_hash) 244 | ftxt += _get_spy_text(method_hash) 245 | 246 | if(_stubber and method_hash.name != '_init'): 247 | ftxt += "\treturn __gut_metadata_.stubber.get_return(self, '" + method_hash.name + "', " + called_with + ")\n" 248 | else: 249 | ftxt += "\tpass\n" 250 | 251 | return ftxt 252 | 253 | func _get_super_func_text(method_hash): 254 | var call_method = _method_maker.get_super_call_text(method_hash) 255 | 256 | var call_super_text = str("return ", call_method, "\n") 257 | 258 | var ftxt = _method_maker.get_decleration_text(method_hash) + "\n" 259 | ftxt += _get_spy_text(method_hash) 260 | 261 | ftxt += _get_indented_line(1, call_super_text) 262 | 263 | return ftxt 264 | 265 | # returns the path to write the double file to 266 | func _get_temp_path(object_info): 267 | var file_name = object_info.get_path().get_file().get_basename() 268 | var extension = object_info.get_path().get_extension() 269 | 270 | if(object_info.has_subpath()): 271 | file_name += '__' + object_info.get_subpath().replace('/', '__') 272 | 273 | if(_use_unique_names): 274 | file_name += str('__dbl', _double_count, '__.', extension) 275 | else: 276 | file_name += '.' + extension 277 | 278 | var to_return = _output_dir.plus_file(file_name) 279 | return to_return 280 | 281 | func _double(obj_info, override_path=null): 282 | var temp_path = _get_temp_path(obj_info) 283 | _write_file(obj_info, temp_path, override_path) 284 | _double_count += 1 285 | return temp_path 286 | 287 | # ############### 288 | # Public 289 | # ############### 290 | func get_output_dir(): 291 | return _output_dir 292 | 293 | func set_output_dir(output_dir): 294 | _output_dir = output_dir 295 | var d = Directory.new() 296 | d.make_dir_recursive(output_dir) 297 | 298 | func get_spy(): 299 | return _spy 300 | 301 | func set_spy(spy): 302 | _spy = spy 303 | 304 | func get_stubber(): 305 | return _stubber 306 | 307 | func set_stubber(stubber): 308 | _stubber = stubber 309 | 310 | func get_logger(): 311 | return _lgr 312 | 313 | func set_logger(logger): 314 | _lgr = logger 315 | _method_maker.set_logger(logger) 316 | 317 | func get_strategy(): 318 | return _strategy 319 | 320 | func set_strategy(strategy): 321 | _strategy = strategy 322 | 323 | # double a scene 324 | func double_scene(path, strategy=_strategy): 325 | _temp_strategy(strategy) 326 | 327 | var oi = ObjectInfo.new(path) 328 | var temp_path = _get_temp_path(oi) 329 | _double_scene_and_script(path, temp_path) 330 | 331 | _restore_strategy() 332 | return load(temp_path) 333 | 334 | # double a script/object 335 | func double(path, strategy=_strategy): 336 | _temp_strategy(strategy) 337 | 338 | var oi = ObjectInfo.new(path) 339 | var to_return = load(_double(oi)) 340 | 341 | _restore_strategy() 342 | return to_return 343 | 344 | # double an inner class in a script 345 | func double_inner(path, subpath, strategy=_strategy): 346 | _temp_strategy(strategy) 347 | 348 | var oi = ObjectInfo.new(path, subpath) 349 | var to_return = load(_double(oi)) 350 | 351 | _restore_strategy() 352 | return to_return 353 | 354 | func clear_output_directory(): 355 | var did = false 356 | if(_output_dir.find('user://') == 0): 357 | var d = Directory.new() 358 | var result = d.open(_output_dir) 359 | # BIG GOTCHA HERE. If it cannot open the dir w/ erro 31, then the 360 | # directory becomes res:// and things go on normally and gut clears out 361 | # out res:// which is SUPER BAD. 362 | if(result == OK): 363 | d.list_dir_begin(true) 364 | var files = [] 365 | var f = d.get_next() 366 | while(f != ''): 367 | d.remove(f) 368 | f = d.get_next() 369 | did = true 370 | return did 371 | 372 | func delete_output_directory(): 373 | var did = clear_output_directory() 374 | if(did): 375 | var d = Directory.new() 376 | d.remove(_output_dir) 377 | 378 | # When creating doubles a unique name is used that each double can be its own 379 | # thing. Sometimes, for testing, we do not want to do this so this allows 380 | # you to turn off creating unique names for each double class. 381 | # 382 | # THIS SHOULD NEVER BE USED OUTSIDE OF INTERNAL GUT TESTING. It can cause 383 | # weird, hard to track down problems. 384 | func set_use_unique_names(should): 385 | _use_unique_names = should 386 | -------------------------------------------------------------------------------- /addons/gut/gut_cmdln.gd: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | #(G)odot (U)nit (T)est class 3 | # 4 | ################################################################################ 5 | #The MIT License (MIT) 6 | #===================== 7 | # 8 | #Copyright (c) 2019 Tom "Butch" Wesley 9 | # 10 | #Permission is hereby granted, free of charge, to any person obtaining a copy 11 | #of this software and associated documentation files (the "Software"), to deal 12 | #in the Software without restriction, including without limitation the rights 13 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | #copies of the Software, and to permit persons to whom the Software is 15 | #furnished to do so, subject to the following conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be included in 18 | #all copies or substantial portions of the Software. 19 | # 20 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | #THE SOFTWARE. 27 | # 28 | ################################################################################ 29 | # Description 30 | # ----------- 31 | # Command line interface for the GUT unit testing tool. Allows you to run tests 32 | # from the command line instead of running a scene. Place this script along with 33 | # gut.gd into your scripts directory at the root of your project. Once there you 34 | # can run this script (from the root of your project) using the following command: 35 | # godot -s -d test/gut/gut_cmdln.gd 36 | # 37 | # See the readme for a list of options and examples. You can also use the -gh 38 | # option to get more information about how to use the command line interface. 39 | # 40 | # Version 6.7.0 41 | ################################################################################ 42 | extends SceneTree 43 | 44 | 45 | var Optparse = load('res://addons/gut/optparse.gd') 46 | #------------------------------------------------------------------------------- 47 | # Helper class to resolve the various different places where an option can 48 | # be set. Using the get_value method will enforce the order of precedence of: 49 | # 1. command line value 50 | # 2. config file value 51 | # 3. default value 52 | # 53 | # The idea is that you set the base_opts. That will get you a copies of the 54 | # hash with null values for the other types of values. Lower precedented hashes 55 | # will punch through null values of higher precedented hashes. 56 | #------------------------------------------------------------------------------- 57 | class OptionResolver: 58 | var base_opts = null 59 | var cmd_opts = null 60 | var config_opts = null 61 | 62 | 63 | func get_value(key): 64 | return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key])) 65 | 66 | func set_base_opts(opts): 67 | base_opts = opts 68 | cmd_opts = _null_copy(opts) 69 | config_opts = _null_copy(opts) 70 | 71 | # creates a copy of a hash with all values null. 72 | func _null_copy(h): 73 | var new_hash = {} 74 | for key in h: 75 | new_hash[key] = null 76 | return new_hash 77 | 78 | func _nvl(a, b): 79 | if(a == null): 80 | return b 81 | else: 82 | return a 83 | func _string_it(h): 84 | var to_return = '' 85 | for key in h: 86 | to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')') 87 | return to_return 88 | 89 | func to_s(): 90 | return str("base:\n", _string_it(base_opts), "\n", \ 91 | "config:\n", _string_it(config_opts), "\n", \ 92 | "cmd:\n", _string_it(cmd_opts), "\n", \ 93 | "resolved:\n", _string_it(get_resolved_values())) 94 | 95 | func get_resolved_values(): 96 | var to_return = {} 97 | for key in base_opts: 98 | to_return[key] = get_value(key) 99 | return to_return 100 | 101 | func to_s_verbose(): 102 | var to_return = '' 103 | var resolved = get_resolved_values() 104 | for key in base_opts: 105 | to_return += str(key, "\n") 106 | to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n") 107 | to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n") 108 | to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n") 109 | to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n") 110 | 111 | return to_return 112 | 113 | #------------------------------------------------------------------------------- 114 | # Here starts the actual script that uses the Options class to kick off Gut 115 | # and run your tests. 116 | #------------------------------------------------------------------------------- 117 | var _utils = load('res://addons/gut/utils.gd').new() 118 | # instance of gut 119 | var _tester = null 120 | # array of command line options specified 121 | var _opts = [] 122 | # Hash for easier access to the options in the code. Options will be 123 | # extracted into this hash and then the hash will be used afterwards so 124 | # that I don't make any dumb typos and get the neat code-sense when I 125 | # type a dot. 126 | var options = { 127 | config_file = 'res://.gutconfig.json', 128 | dirs = [], 129 | double_strategy = 'partial', 130 | ignore_pause = false, 131 | include_subdirs = false, 132 | inner_class = '', 133 | log_level = 1, 134 | opacity = 100, 135 | prefix = 'test_', 136 | selected = '', 137 | should_exit = false, 138 | should_maximize = false, 139 | show_help = false, 140 | suffix = '.gd', 141 | tests = [], 142 | unit_test_name = '', 143 | } 144 | 145 | # flag to indicate if only a single script should be run. 146 | var _run_single = false 147 | 148 | func setup_options(): 149 | var opts = Optparse.new() 150 | opts.set_banner(('This is the command line interface for the unit testing tool Gut. With this ' + 151 | 'interface you can run one or more test scripts from the command line. In order ' + 152 | 'for the Gut options to not clash with any other godot options, each option starts ' + 153 | 'with a "g". Also, any option that requires a value will take the form of ' + 154 | '"-g=". There cannot be any spaces between the option, the "=", or ' + 155 | 'inside a specified value or godot will think you are trying to run a scene.')) 156 | opts.add('-gtest', [], 'Comma delimited list of full paths to test scripts to run.') 157 | opts.add('-gdir', [], 'Comma delimited list of directories to add tests from.') 158 | opts.add('-gprefix', 'test_', 'Prefix used to find tests when specifying -gdir. Default "[default]"') 159 | opts.add('-gsuffix', '.gd', 'Suffix used to find tests when specifying -gdir. Default "[default]"') 160 | opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.') 161 | opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.') 162 | opts.add('-glog', 1, 'Log level. Default [default]') 163 | opts.add('-gignore_pause', false, 'Ignores any calls to gut.pause_before_teardown.') 164 | opts.add('-gselect', '', ('Select a script to run initially. The first script that ' + 165 | 'was loaded using -gtest or -gdir that contains the specified ' + 166 | 'string will be executed. You may run others by interacting ' + 167 | 'with the GUI.')) 168 | opts.add('-gunit_test_name', '', ('Name of a test to run. Any test that contains the specified ' + 169 | 'text will be run, all others will be skipped.')) 170 | opts.add('-gh', false, 'Print this help, then quit') 171 | opts.add('-gconfig', 'res://.gutconfig.json', 'A config file that contains configuration information. Default is res://.gutconfig.json') 172 | opts.add('-ginner_class', '', 'Only run inner classes that contain this string') 173 | opts.add('-gopacity', 100, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') 174 | opts.add('-gpo', false, 'Print option values from all sources and the value used, then quit.') 175 | opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.') 176 | opts.add('-gdouble_strategy', 'partial', 'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"') 177 | opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file then quit.') 178 | return opts 179 | 180 | 181 | # Parses options, applying them to the _tester or setting values 182 | # in the options struct. 183 | func extract_command_line_options(from, to): 184 | to.tests = from.get_value('-gtest') 185 | to.dirs = from.get_value('-gdir') 186 | to.should_exit = from.get_value('-gexit') 187 | to.should_maximize = from.get_value('-gmaximize') 188 | to.log_level = from.get_value('-glog') 189 | to.ignore_pause = from.get_value('-gignore_pause') 190 | to.selected = from.get_value('-gselect') 191 | to.prefix = from.get_value('-gprefix') 192 | to.suffix = from.get_value('-gsuffix') 193 | to.unit_test_name = from.get_value('-gunit_test_name') 194 | to.config_file = from.get_value('-gconfig') 195 | to.inner_class = from.get_value('-ginner_class') 196 | to.opacity = from.get_value('-gopacity') 197 | to.include_subdirs = from.get_value('-ginclude_subdirs') 198 | to.double_strategy = from.get_value('-gdouble_strategy') 199 | 200 | 201 | func load_options_from_config_file(file_path, into): 202 | # SHORTCIRCUIT 203 | var f = File.new() 204 | if(!f.file_exists(file_path)): 205 | if(file_path != 'res://.gutconfig.json'): 206 | print('ERROR: Config File "', file_path, '" does not exist.') 207 | return -1 208 | else: 209 | return 1 210 | 211 | f.open(file_path, f.READ) 212 | var json = f.get_as_text() 213 | f.close() 214 | 215 | var results = JSON.parse(json) 216 | # SHORTCIRCUIT 217 | if(results.error != OK): 218 | print("\n\n",'!! ERROR parsing file: ', file_path) 219 | print(' at line ', results.error_line, ':') 220 | print(' ', results.error_string) 221 | return -1 222 | 223 | # Get all the options out of the config file using the option name. The 224 | # options hash is now the default source of truth for the name of an option. 225 | for key in into: 226 | if(results.result.has(key)): 227 | into[key] = results.result[key] 228 | 229 | return 1 230 | 231 | # Apply all the options specified to _tester. This is where the rubber meets 232 | # the road. 233 | func apply_options(opts): 234 | _tester = load('res://addons/gut/gut.gd').new() 235 | get_root().add_child(_tester) 236 | _tester.connect('tests_finished', self, '_on_tests_finished', [opts.should_exit]) 237 | _tester.set_yield_between_tests(true) 238 | _tester.set_modulate(Color(1.0, 1.0, 1.0, min(1.0, float(opts.opacity) / 100))) 239 | _tester.show() 240 | 241 | _tester.set_include_subdirectories(opts.include_subdirs) 242 | 243 | if(opts.should_maximize): 244 | _tester.maximize() 245 | 246 | if(opts.inner_class != ''): 247 | _tester.set_inner_class_name(opts.inner_class) 248 | _tester.set_log_level(opts.log_level) 249 | _tester.set_ignore_pause_before_teardown(opts.ignore_pause) 250 | 251 | for i in range(opts.dirs.size()): 252 | _tester.add_directory(opts.dirs[i], opts.prefix, opts.suffix) 253 | 254 | for i in range(opts.tests.size()): 255 | _tester.add_script(opts.tests[i]) 256 | 257 | if(opts.selected != ''): 258 | _tester.select_script(opts.selected) 259 | _run_single = true 260 | 261 | if(opts.double_strategy == 'full'): 262 | _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.FULL) 263 | elif(opts.double_strategy == 'partial'): 264 | _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) 265 | 266 | _tester.set_unit_test_name(opts.unit_test_name) 267 | 268 | func _print_gutconfigs(values): 269 | var header = """Here is a sample of a full .gutconfig.json file. 270 | You do not need to specify all values in your own file. The values supplied in 271 | this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample 272 | option (the resolved values where default < .gutconfig < command line).""" 273 | print("\n", header.replace("\n", ' '), "\n\n") 274 | var resolved = values 275 | 276 | # remove some options that don't make sense to be in config 277 | resolved.erase("config_file") 278 | resolved.erase("show_help") 279 | 280 | print("Here's a config with all the properties set based off of your current command and config.") 281 | var text = JSON.print(resolved) 282 | print(text.replace(',', ",\n")) 283 | 284 | for key in resolved: 285 | resolved[key] = null 286 | 287 | print("\n\nAnd here's an empty config for you fill in what you want.") 288 | text = JSON.print(resolved) 289 | print(text.replace(',', ",\n")) 290 | 291 | 292 | # parse options and run Gut 293 | func _init(): 294 | var opt_resolver = OptionResolver.new() 295 | opt_resolver.set_base_opts(options) 296 | 297 | print("\n\n", ' --- Gut ---') 298 | var o = setup_options() 299 | 300 | var all_options_valid = o.parse() 301 | extract_command_line_options(o, opt_resolver.cmd_opts) 302 | var load_result = \ 303 | load_options_from_config_file(opt_resolver.get_value('config_file'), opt_resolver.config_opts) 304 | 305 | if(load_result == -1): # -1 indicates json parse error 306 | quit() 307 | else: 308 | if(!all_options_valid): 309 | quit() 310 | elif(o.get_value('-gh')): 311 | o.print_help() 312 | quit() 313 | elif(o.get_value('-gpo')): 314 | print('All command line options and where they are specified. ' + 315 | 'The "final" value shows which value will actually be used ' + 316 | 'based on order of precedence (default < .gutconfig < cmd line).' + "\n") 317 | print(opt_resolver.to_s_verbose()) 318 | quit() 319 | elif(o.get_value('-gprint_gutconfig_sample')): 320 | _print_gutconfigs(opt_resolver.get_resolved_values()) 321 | quit() 322 | else: 323 | apply_options(opt_resolver.get_resolved_values()) 324 | _tester.test_scripts(!_run_single) 325 | 326 | # exit if option is set. 327 | func _on_tests_finished(should_exit): 328 | if(_tester.get_fail_count()): 329 | OS.exit_code = 1 330 | 331 | if(should_exit): 332 | quit() 333 | -------------------------------------------------------------------------------- /HexGrid.gd: -------------------------------------------------------------------------------- 1 | """ 2 | A converter between hex and Godot-space coordinate systems. 3 | 4 | The hex grid uses +x => NE and +y => N, whereas 5 | the projection to Godot-space uses +x => E, +y => S. 6 | 7 | We map hex coordinates to Godot-space with +y flipped to be the down vector 8 | so that it maps neatly to both Godot's 2D coordinate system, and also to 9 | x,z planes in 3D space. 10 | 11 | 12 | ## Usage: 13 | 14 | #### var hex_scale = Vector2(...) 15 | 16 | If you want your hexes to display larger than the default 1 x 0.866 units, 17 | then you can customise the scale of the hexes using this property. 18 | 19 | #### func get_hex_center(hex) 20 | 21 | Returns the Godot-space Vector2 of the center of the given hex. 22 | 23 | The coordinates can be given as either a HexCell instance; a Vector3 cube 24 | coordinate, or a Vector2 axial coordinate. 25 | 26 | #### func get_hex_center3(hex [, y]) 27 | 28 | Returns the Godot-space Vector3 of the center of the given hex. 29 | 30 | The coordinates can be given as either a HexCell instance; a Vector3 cube 31 | coordinate, or a Vector2 axial coordinate. 32 | 33 | If a second parameter is given, it will be used for the y value in the 34 | returned Vector3. Otherwise, the y value will be 0. 35 | 36 | #### func get_hex_at(coords) 37 | 38 | Returns HexCell whose grid position contains the given Godot-space coordinates. 39 | 40 | The given value can either be a Vector2 on the grid's plane 41 | or a Vector3, in which case its (x, z) coordinates will be used. 42 | 43 | 44 | ### Path-finding 45 | 46 | HexGrid also includes an implementation of the A* pathfinding algorithm. 47 | The class can be used to populate an internal representation of a game grid 48 | with obstacles to traverse. 49 | 50 | #### func set_bounds(min_coords, max_coords) 51 | 52 | Sets the hard outer limits of the path-finding grid. 53 | 54 | The coordinates given are the min and max corners *inside* a bounding 55 | square (diamond in hex visualisation) region. Any hex outside that area 56 | is considered an impassable obstacle. 57 | 58 | The default bounds consider only the origin to be inside, so you're probably 59 | going to want to do something about that. 60 | 61 | #### func get_obstacles() 62 | 63 | Returns a dict of all obstacles and their costs 64 | 65 | The keys are Vector2s of the axial coordinates, the values will be the 66 | cost value. Zero cost means an impassable obstacle. 67 | 68 | #### func add_obstacles(coords, cost=0) 69 | 70 | Adds one or more obstacles to the path-finding grid 71 | 72 | The given coordinates (axial or cube), HexCell instance, or array thereof, 73 | will be added as path-finding obstacles with the given cost. A zero cost 74 | indicates an impassable obstacle. 75 | 76 | #### func remove_obstacles(coords) 77 | 78 | Removes one or more obstacles from the path-finding grid 79 | 80 | The given coordinates (axial or cube), HexCell instance, or array thereof, 81 | will be removed as obstacles from the path-finding grid. 82 | 83 | #### func get_barriers() 84 | 85 | Returns a dict of all barriers in the grid. 86 | 87 | A barrier is an edge of a hex which is either impassable, or has a 88 | non-zero cost to traverse. If two adjacent hexes both have barriers on 89 | their shared edge, their costs are summed. 90 | Barrier costs are in addition to the obstacle (or default) cost of 91 | moving to a hex. 92 | 93 | The outer dict is a mapping of axial coords to an inner barrier dict. 94 | The inner dict maps between HexCell.DIR_* directions and the cost of 95 | travel in that direction. A cost of zero indicates an impassable barrier. 96 | 97 | #### func add_barriers(coords, dirs, cost=0) 98 | 99 | Adds one or more barriers to locations on the grid. 100 | 101 | The given coordinates (axial or cube), HexCell instance, or array thereof, 102 | will have path-finding barriers added in the given HexCell.DIR_* directions 103 | with the given cost. A zero cost indicates an impassable obstacle. 104 | 105 | Existing barriers at given coordinates will not be removed, but will be 106 | overridden if the direction is specified. 107 | 108 | #### func remove_barriers(coords, dirs=null) 109 | 110 | Remove one or more barriers from the path-finding grid. 111 | 112 | The given coordinates (axial or cube), HexCell instance, or array thereof, 113 | will have the path-finding barriers in the supplied HexCell.DIR_* directions 114 | removed. If no direction is specified, all barriers for the given 115 | coordinates will be removed. 116 | 117 | #### func get_hex_cost(coords) 118 | 119 | Returns the cost of moving into the specified grid position. 120 | 121 | Will return 0 if the given grid position is inaccessible. 122 | 123 | #### func get_move_cost(coords, direction) 124 | 125 | Returns the cost of moving from one hex to an adjacent one. 126 | 127 | This method takes into account any barriers defined between the two 128 | hexes, as well as the cost of the target hex. 129 | Will return 0 if the target hex is inaccessible, or if there is an 130 | impassable barrier between the hexes. 131 | 132 | The direction should be provided as one of the HexCell.DIR_* values. 133 | 134 | #### func find_path(start, goal, exceptions=[]) 135 | 136 | Calculates an A* path from the start to the goal. 137 | 138 | Returns a list of HexCell instances charting the path from the given start 139 | coordinates to the goal, including both ends of the journey. 140 | 141 | Exceptions can be specified as the third parameter, and will act as 142 | impassable obstacles for the purposes of this call of the function. 143 | This can be used for pathing around obstacles which may change position 144 | (eg. enemy playing pieces), without having to update the grid's list of 145 | obstacles every time something moves. 146 | 147 | If the goal is an impassable location, the path will terminate at the nearest 148 | adjacent coordinate. In this instance, the goal hex will not be included in 149 | the returned array. 150 | 151 | If there is no path possible to the goal, or any hex adjacent to it, an 152 | empty array is returned. But the algorithm will only know that once it's 153 | visited every tile it can reach, so try not to path to the impossible. 154 | 155 | """ 156 | extends Reference 157 | 158 | var HexCell = preload("./HexCell.gd") 159 | # Duplicate these from HexCell for ease of access 160 | const DIR_N = Vector3(0, 1, -1) 161 | const DIR_NE = Vector3(1, 0, -1) 162 | const DIR_SE = Vector3(1, -1, 0) 163 | const DIR_S = Vector3(0, -1, 1) 164 | const DIR_SW = Vector3(-1, 0, 1) 165 | const DIR_NW = Vector3(-1, 1, 0) 166 | const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW] 167 | 168 | # Allow the user to scale the hex for fake perspective or somesuch 169 | export(Vector2) var hex_scale = Vector2(1, 1) setget set_hex_scale 170 | 171 | var base_hex_size = Vector2(1, sqrt(3)/2) 172 | var hex_size 173 | var hex_transform 174 | var hex_transform_inv 175 | # Pathfinding obstacles {Vector2: cost} 176 | # A zero cost means impassable 177 | var path_obstacles = {} 178 | # Barriers restrict traversing between edges (in either direction) 179 | # costs for barriers and obstacles are cumulative, but impassable is impassable 180 | # {Vector2: {DIR_VECTOR2: cost, ...}} 181 | var path_barriers = {} 182 | var path_bounds = Rect2() 183 | var path_cost_default = 1.0 184 | 185 | 186 | func _init(): 187 | set_hex_scale(hex_scale) 188 | 189 | 190 | func set_hex_scale(scale): 191 | # We need to recalculate some stuff when projection scale changes 192 | hex_scale = scale 193 | hex_size = base_hex_size * hex_scale 194 | hex_transform = Transform2D( 195 | Vector2(hex_size.x * 3/4, -hex_size.y / 2), 196 | Vector2(0, -hex_size.y), 197 | Vector2(0, 0) 198 | ) 199 | hex_transform_inv = hex_transform.affine_inverse() 200 | 201 | 202 | """ 203 | Converting between hex-grid and 2D spatial coordinates 204 | """ 205 | func get_hex_center(hex): 206 | # Returns hex's centre position on the projection plane 207 | hex = HexCell.new(hex) 208 | return hex_transform * hex.axial_coords 209 | 210 | func get_hex_at(coords): 211 | # Returns a HexCell at the given Vector2/3 on the projection plane 212 | # If the given value is a Vector3, its x,z coords will be used 213 | if typeof(coords) == TYPE_VECTOR3: 214 | coords = Vector2(coords.x, coords.z) 215 | return HexCell.new(hex_transform_inv * coords) 216 | 217 | func get_hex_center3(hex, y=0): 218 | # Returns hex's centre position as a Vector3 219 | var coords = get_hex_center(hex) 220 | return Vector3(coords.x, y, coords.y) 221 | 222 | 223 | """ 224 | Pathfinding 225 | 226 | Ref: https://www.redblobgames.com/pathfinding/a-star/introduction.html 227 | 228 | We use axial coords for everything internally (to use Rect2.has_point), 229 | but the methods accept cube or axial coords, or HexCell instances. 230 | """ 231 | func set_bounds(min_coords, max_coords): 232 | # Set the absolute bounds of the pathfinding area in grid coords 233 | # The given coords will be inside the boundary (hence the extra (1, 1)) 234 | min_coords = HexCell.new(min_coords).axial_coords 235 | max_coords = HexCell.new(max_coords).axial_coords 236 | path_bounds = Rect2(min_coords, (max_coords - min_coords) + Vector2(1, 1)) 237 | 238 | func get_obstacles(): 239 | return path_obstacles 240 | 241 | func add_obstacles(vals, cost=0): 242 | # Store the given coordinate/s as obstacles 243 | if not typeof(vals) == TYPE_ARRAY: 244 | vals = [vals] 245 | for coords in vals: 246 | coords = HexCell.new(coords).axial_coords 247 | path_obstacles[coords] = cost 248 | 249 | func remove_obstacles(vals): 250 | # Remove the given obstacle/s from the grid 251 | if not typeof(vals) == TYPE_ARRAY: 252 | vals = [vals] 253 | for coords in vals: 254 | coords = HexCell.new(coords).axial_coords 255 | path_obstacles.erase(coords) 256 | 257 | func get_barriers(): 258 | return path_barriers 259 | 260 | func add_barriers(vals, dirs, cost=0): 261 | # Store the given directions of the given locations as barriers 262 | if not typeof(vals) == TYPE_ARRAY: 263 | vals = [vals] 264 | if not typeof(dirs) == TYPE_ARRAY: 265 | dirs = [dirs] 266 | for coords in vals: 267 | coords = HexCell.new(coords).axial_coords 268 | var barriers = {} 269 | if coords in path_barriers: 270 | # Already something there 271 | barriers = path_barriers[coords] 272 | else: 273 | path_barriers[coords] = barriers 274 | # Set or override the given dirs 275 | for dir in dirs: 276 | barriers[dir] = cost 277 | path_barriers[coords] = barriers 278 | 279 | func remove_barriers(vals, dirs=null): 280 | if not typeof(vals) == TYPE_ARRAY: 281 | vals = [vals] 282 | if dirs != null and not typeof(dirs) == TYPE_ARRAY: 283 | dirs = [dirs] 284 | for coords in vals: 285 | coords = HexCell.new(coords).axial_coords 286 | if dirs == null: 287 | path_barriers.erase(coords) 288 | else: 289 | for dir in dirs: 290 | path_barriers[coords].erase(dir) 291 | 292 | 293 | func get_hex_cost(coords): 294 | # Returns the cost of moving to the given hex 295 | coords = HexCell.new(coords).axial_coords 296 | if coords in path_obstacles: 297 | return path_obstacles[coords] 298 | if not path_bounds.has_point(coords): 299 | # Out of bounds 300 | return 0 301 | return path_cost_default 302 | 303 | func get_move_cost(coords, direction): 304 | # Returns the cost of moving from one hex to a neighbour 305 | direction = HexCell.new(direction).cube_coords 306 | var start_hex = HexCell.new(coords) 307 | var target_hex = HexCell.new(start_hex.cube_coords + direction) 308 | coords = start_hex.axial_coords 309 | # First check if either end is completely impassable 310 | var cost = get_hex_cost(start_hex) 311 | if cost == 0: 312 | return 0 313 | cost = get_hex_cost(target_hex) 314 | if cost == 0: 315 | return 0 316 | # Check for barriers 317 | var barrier_cost 318 | if coords in path_barriers and direction in path_barriers[coords]: 319 | barrier_cost = path_barriers[coords][direction] 320 | if barrier_cost == 0: 321 | return 0 322 | cost += barrier_cost 323 | var target_coords = target_hex.axial_coords 324 | if target_coords in path_barriers and -direction in path_barriers[target_coords]: 325 | barrier_cost = path_barriers[target_coords][-direction] 326 | if barrier_cost == 0: 327 | return 0 328 | cost += barrier_cost 329 | return cost 330 | 331 | 332 | func get_path(start, goal, exceptions=[]): 333 | # DEPRECATED! 334 | # The function `get_path` is used by Godot for something completely different, 335 | # so we renamed it here to `find_path`. 336 | push_warning("HexGrid.get_path has been deprecated, use find_path instead.") 337 | return find_path(start, goal, exceptions) 338 | 339 | func find_path(start, goal, exceptions=[]): 340 | # Light a starry path from the start to the goal, inclusive 341 | start = HexCell.new(start).axial_coords 342 | goal = HexCell.new(goal).axial_coords 343 | # Make sure all the exceptions are axial coords 344 | var exc = [] 345 | for ex in exceptions: 346 | exc.append(HexCell.new(ex).axial_coords) 347 | exceptions = exc 348 | # Now we begin the A* search 349 | var frontier = [make_priority_item(start, 0)] 350 | var came_from = {start: null} 351 | var cost_so_far = {start: 0} 352 | while not frontier.empty(): 353 | var current = frontier.pop_front().v 354 | if current == goal: 355 | break 356 | for next_hex in HexCell.new(current).get_all_adjacent(): 357 | var next = next_hex.axial_coords 358 | var next_cost = get_move_cost(current, next - current) 359 | if next == goal and (next in exceptions or get_hex_cost(next) == 0): 360 | # Our goal is an obstacle, but we're next to it 361 | # so our work here is done 362 | came_from[next] = current 363 | frontier.clear() 364 | break 365 | if not next_cost or next in exceptions: 366 | # We shall not pass 367 | continue 368 | next_cost += cost_so_far[current] 369 | if not next in cost_so_far or next_cost < cost_so_far[next]: 370 | # New shortest path to that node 371 | cost_so_far[next] = next_cost 372 | var priority = next_cost + next_hex.distance_to(goal) 373 | # Insert into the frontier 374 | var item = make_priority_item(next, priority) 375 | var idx = frontier.bsearch_custom(item, self, "comp_priority_item") 376 | frontier.insert(idx, item) 377 | came_from[next] = current 378 | 379 | if not goal in came_from: 380 | # Not found 381 | return [] 382 | # Follow the path back where we came_from 383 | var path = [] 384 | if not (get_hex_cost(goal) == 0 or goal in exceptions): 385 | # We only include the goal if it's traversable 386 | path.append(HexCell.new(goal)) 387 | var current = goal 388 | while current != start: 389 | current = came_from[current] 390 | path.push_front(HexCell.new(current)) 391 | return path 392 | 393 | # Used to make a priority queue out of an array 394 | func make_priority_item(val, priority): 395 | return {"v": val, "p": priority} 396 | func comp_priority_item(a, b): 397 | return a.p < b.p 398 | -------------------------------------------------------------------------------- /addons/gut/gut_gui.gd: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | #The MIT License (MIT) 3 | #===================== 4 | # 5 | #Copyright (c) 2017 Tom "Butch" Wesley 6 | # 7 | #Permission is hereby granted, free of charge, to any person obtaining a copy 8 | #of this software and associated documentation files (the "Software"), to deal 9 | #in the Software without restriction, including without limitation the rights 10 | #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | #copies of the Software, and to permit persons to whom the Software is 12 | #furnished to do so, subject to the following conditions: 13 | # 14 | #The above copyright notice and this permission notice shall be included in 15 | #all copies or substantial portions of the Software. 16 | # 17 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | #THE SOFTWARE. 24 | # 25 | ################################################################################ 26 | 27 | ################################################################################ 28 | # This class contains all the GUI creation code for Gut. It was split out and 29 | # hopefully can be moved to a scene in the future. 30 | ################################################################################ 31 | extends WindowDialog 32 | 33 | # various counters. Most have been moved to the Summary object but not all. 34 | var _summary = { 35 | moved_methods = 0, 36 | # these are used to display the tally in the top right corner. Since the 37 | # implementation changed to summing things up at the end, the running 38 | # update wasn't showing. Hack. 39 | tally_passed = 0, 40 | tally_failed = 0 41 | } 42 | 43 | var _is_running = false 44 | var min_size = Vector2(650, 400) 45 | 46 | #controls 47 | var _ctrls = { 48 | text_box = TextEdit.new(), 49 | run_button = Button.new(), 50 | copy_button = Button.new(), 51 | clear_button = Button.new(), 52 | continue_button = Button.new(), 53 | log_level_slider = HSlider.new(), 54 | scripts_drop_down = OptionButton.new(), 55 | next_button = Button.new(), 56 | previous_button = Button.new(), 57 | stop_button = Button.new(), 58 | script_progress = ProgressBar.new(), 59 | test_progress = ProgressBar.new(), 60 | runtime_label = Label.new(), 61 | ignore_continue_checkbox = CheckBox.new(), 62 | pass_count = Label.new(), 63 | run_rest = Button.new() 64 | } 65 | 66 | var _mouse_down = false 67 | var _mouse_down_pos = null 68 | var _mouse_in = false 69 | 70 | func _set_anchor_top_right(obj): 71 | obj.set_anchor(MARGIN_RIGHT, ANCHOR_BEGIN) 72 | obj.set_anchor(MARGIN_LEFT, ANCHOR_END) 73 | obj.set_anchor(MARGIN_TOP, ANCHOR_BEGIN) 74 | 75 | func _set_anchor_bottom_right(obj): 76 | obj.set_anchor(MARGIN_LEFT, ANCHOR_END) 77 | obj.set_anchor(MARGIN_RIGHT, ANCHOR_END) 78 | obj.set_anchor(MARGIN_TOP, ANCHOR_END) 79 | obj.set_anchor(MARGIN_BOTTOM, ANCHOR_END) 80 | 81 | func _set_anchor_bottom_left(obj): 82 | obj.set_anchor(MARGIN_LEFT, ANCHOR_BEGIN) 83 | obj.set_anchor(MARGIN_TOP, ANCHOR_END) 84 | obj.set_anchor(MARGIN_TOP, ANCHOR_END) 85 | 86 | #------------------------------------------------------------------------------- 87 | #------------------------------------------------------------------------------- 88 | func setup_controls(): 89 | var button_size = Vector2(75, 35) 90 | var button_spacing = Vector2(10, 0) 91 | var pos = Vector2(0, 0) 92 | 93 | add_child(_ctrls.text_box) 94 | _ctrls.text_box.set_size(Vector2(get_size().x - 4, 300)) 95 | _ctrls.text_box.set_position(Vector2(2, 0)) 96 | _ctrls.text_box.set_readonly(true) 97 | _ctrls.text_box.set_syntax_coloring(true) 98 | _ctrls.text_box.set_anchor(MARGIN_LEFT, ANCHOR_BEGIN) 99 | _ctrls.text_box.set_anchor(MARGIN_RIGHT, ANCHOR_END) 100 | _ctrls.text_box.set_anchor(MARGIN_TOP, ANCHOR_BEGIN) 101 | _ctrls.text_box.set_anchor(MARGIN_BOTTOM, ANCHOR_END) 102 | 103 | add_child(_ctrls.copy_button) 104 | _ctrls.copy_button.set_text("Copy") 105 | _ctrls.copy_button.set_size(button_size) 106 | _ctrls.copy_button.set_position(Vector2(get_size().x - 5 - button_size.x, _ctrls.text_box.get_size().y + 10)) 107 | _set_anchor_bottom_right(_ctrls.copy_button) 108 | 109 | add_child(_ctrls.clear_button) 110 | _ctrls.clear_button.set_text("Clear") 111 | _ctrls.clear_button.set_size(button_size) 112 | _ctrls.clear_button.set_position(_ctrls.copy_button.get_position() - Vector2(button_size.x, 0) - button_spacing) 113 | _set_anchor_bottom_right(_ctrls.clear_button) 114 | 115 | add_child(_ctrls.pass_count) 116 | _ctrls.pass_count.set_text('0 - 0') 117 | _ctrls.pass_count.set_size(Vector2(100, 30)) 118 | _ctrls.pass_count.set_position(Vector2(550, 0)) 119 | _ctrls.pass_count.set_align(HALIGN_RIGHT) 120 | _set_anchor_top_right(_ctrls.pass_count) 121 | 122 | add_child(_ctrls.continue_button) 123 | _ctrls.continue_button.set_text("Continue") 124 | _ctrls.continue_button.set_size(Vector2(100, 25)) 125 | _ctrls.continue_button.set_position(Vector2(_ctrls.clear_button.get_position().x, _ctrls.clear_button.get_position().y + _ctrls.clear_button.get_size().y + 10)) 126 | _ctrls.continue_button.set_disabled(true) 127 | _set_anchor_bottom_right(_ctrls.continue_button) 128 | 129 | add_child(_ctrls.ignore_continue_checkbox) 130 | _ctrls.ignore_continue_checkbox.set_text("Ignore pauses") 131 | #_ctrls.ignore_continue_checkbox.set_pressed(_ignore_pause_before_teardown) 132 | _ctrls.ignore_continue_checkbox.set_size(Vector2(50, 30)) 133 | _ctrls.ignore_continue_checkbox.set_position(Vector2(_ctrls.continue_button.get_position().x, _ctrls.continue_button.get_position().y + _ctrls.continue_button.get_size().y - 5)) 134 | _set_anchor_bottom_right(_ctrls.ignore_continue_checkbox) 135 | 136 | var log_label = Label.new() 137 | add_child(log_label) 138 | log_label.set_text("Log Level") 139 | log_label.set_position(Vector2(10, _ctrls.text_box.get_size().y + 1)) 140 | _set_anchor_bottom_left(log_label) 141 | 142 | add_child(_ctrls.log_level_slider) 143 | _ctrls.log_level_slider.set_size(Vector2(75, 30)) 144 | _ctrls.log_level_slider.set_position(Vector2(10, log_label.get_position().y + 20)) 145 | _ctrls.log_level_slider.set_min(0) 146 | _ctrls.log_level_slider.set_max(2) 147 | _ctrls.log_level_slider.set_ticks(3) 148 | _ctrls.log_level_slider.set_ticks_on_borders(true) 149 | _ctrls.log_level_slider.set_step(1) 150 | #_ctrls.log_level_slider.set_rounded_values(true) 151 | #_ctrls.log_level_slider.set_value(_log_level) 152 | _set_anchor_bottom_left(_ctrls.log_level_slider) 153 | 154 | var script_prog_label = Label.new() 155 | add_child(script_prog_label) 156 | script_prog_label.set_position(Vector2(100, log_label.get_position().y)) 157 | script_prog_label.set_text('Scripts:') 158 | _set_anchor_bottom_left(script_prog_label) 159 | 160 | add_child(_ctrls.script_progress) 161 | _ctrls.script_progress.set_size(Vector2(200, 10)) 162 | _ctrls.script_progress.set_position(script_prog_label.get_position() + Vector2(70, 0)) 163 | _ctrls.script_progress.set_min(0) 164 | _ctrls.script_progress.set_max(1) 165 | _ctrls.script_progress.set_step(1) 166 | _set_anchor_bottom_left(_ctrls.script_progress) 167 | 168 | var test_prog_label = Label.new() 169 | add_child(test_prog_label) 170 | test_prog_label.set_position(Vector2(100, log_label.get_position().y + 15)) 171 | test_prog_label.set_text('Tests:') 172 | _set_anchor_bottom_left(test_prog_label) 173 | 174 | add_child(_ctrls.test_progress) 175 | _ctrls.test_progress.set_size(Vector2(200, 10)) 176 | _ctrls.test_progress.set_position(test_prog_label.get_position() + Vector2(70, 0)) 177 | _ctrls.test_progress.set_min(0) 178 | _ctrls.test_progress.set_max(1) 179 | _ctrls.test_progress.set_step(1) 180 | _set_anchor_bottom_left(_ctrls.test_progress) 181 | 182 | add_child(_ctrls.previous_button) 183 | _ctrls.previous_button.set_size(Vector2(50, 25)) 184 | pos = _ctrls.test_progress.get_position() + Vector2(250, 25) 185 | pos.x -= 300 186 | _ctrls.previous_button.set_position(pos) 187 | _ctrls.previous_button.set_text("<") 188 | _set_anchor_bottom_left(_ctrls.previous_button) 189 | 190 | add_child(_ctrls.stop_button) 191 | _ctrls.stop_button.set_size(Vector2(50, 25)) 192 | pos.x += 60 193 | _ctrls.stop_button.set_position(pos) 194 | _ctrls.stop_button.set_text('stop') 195 | _set_anchor_bottom_left(_ctrls.stop_button) 196 | 197 | add_child(_ctrls.run_rest) 198 | _ctrls.run_rest.set_text('run') 199 | _ctrls.run_rest.set_size(Vector2(50, 25)) 200 | pos.x += 60 201 | _ctrls.run_rest.set_position(pos) 202 | _set_anchor_bottom_left(_ctrls.run_rest) 203 | 204 | add_child(_ctrls.next_button) 205 | _ctrls.next_button.set_size(Vector2(50, 25)) 206 | pos.x += 60 207 | _ctrls.next_button.set_position(pos) 208 | _ctrls.next_button.set_text(">") 209 | _set_anchor_bottom_left(_ctrls.next_button) 210 | 211 | add_child(_ctrls.runtime_label) 212 | _ctrls.runtime_label.set_text('0.0') 213 | _ctrls.runtime_label.set_size(Vector2(50, 30)) 214 | _ctrls.runtime_label.set_position(Vector2(_ctrls.clear_button.get_position().x - 90, _ctrls.next_button.get_position().y)) 215 | _set_anchor_bottom_right(_ctrls.runtime_label) 216 | 217 | # the drop down has to be one of the last added so that when then list of 218 | # scripts is displayed, other controls do not get in the way of selecting 219 | # an item in the list. 220 | add_child(_ctrls.scripts_drop_down) 221 | _ctrls.scripts_drop_down.set_size(Vector2(375, 25)) 222 | _ctrls.scripts_drop_down.set_position(Vector2(10, _ctrls.log_level_slider.get_position().y + 50)) 223 | _set_anchor_bottom_left(_ctrls.scripts_drop_down) 224 | _ctrls.scripts_drop_down.set_clip_text(true) 225 | 226 | add_child(_ctrls.run_button) 227 | _ctrls.run_button.set_text('<- run') 228 | _ctrls.run_button.set_size(Vector2(50, 25)) 229 | _ctrls.run_button.set_position(_ctrls.scripts_drop_down.get_position() + Vector2(_ctrls.scripts_drop_down.get_size().x + 5, 0)) 230 | _set_anchor_bottom_left(_ctrls.run_button) 231 | 232 | func set_it_up(): 233 | self.set_size(min_size) 234 | setup_controls() 235 | self.connect("mouse_entered", self, "_on_mouse_enter") 236 | self.connect("mouse_exited", self, "_on_mouse_exit") 237 | set_process(true) 238 | set_pause_mode(PAUSE_MODE_PROCESS) 239 | _update_controls() 240 | 241 | #------------------------------------------------------------------------------- 242 | # Updates the display 243 | #------------------------------------------------------------------------------- 244 | func _update_controls(): 245 | 246 | if(_is_running): 247 | _ctrls.previous_button.set_disabled(true) 248 | _ctrls.next_button.set_disabled(true) 249 | _ctrls.pass_count.show() 250 | else: 251 | _ctrls.previous_button.set_disabled(_ctrls.scripts_drop_down.get_selected() <= 0) 252 | _ctrls.next_button.set_disabled(_ctrls.scripts_drop_down.get_selected() != -1 and _ctrls.scripts_drop_down.get_selected() == _ctrls.scripts_drop_down.get_item_count() -1) 253 | _ctrls.pass_count.hide() 254 | 255 | # disabled during run 256 | _ctrls.run_button.set_disabled(_is_running) 257 | _ctrls.run_rest.set_disabled(_is_running) 258 | _ctrls.scripts_drop_down.set_disabled(_is_running) 259 | 260 | # enabled during run 261 | _ctrls.stop_button.set_disabled(!_is_running) 262 | _ctrls.pass_count.set_text(str( _summary.tally_passed, ' - ', _summary.tally_failed)) 263 | 264 | 265 | #------------------------------------------------------------------------------- 266 | #detect mouse movement 267 | #------------------------------------------------------------------------------- 268 | func _on_mouse_enter(): 269 | _mouse_in = true 270 | 271 | #------------------------------------------------------------------------------- 272 | #detect mouse movement 273 | #------------------------------------------------------------------------------- 274 | func _on_mouse_exit(): 275 | _mouse_in = false 276 | _mouse_down = false 277 | 278 | 279 | #------------------------------------------------------------------------------- 280 | #Send text box text to clipboard 281 | #------------------------------------------------------------------------------- 282 | func _copy_button_pressed(): 283 | _ctrls.text_box.select_all() 284 | _ctrls.text_box.copy() 285 | 286 | 287 | #------------------------------------------------------------------------------- 288 | #------------------------------------------------------------------------------- 289 | func _init_run(): 290 | _ctrls.text_box.clear_colors() 291 | _ctrls.text_box.add_keyword_color("PASSED", Color(0, 1, 0)) 292 | _ctrls.text_box.add_keyword_color("FAILED", Color(1, 0, 0)) 293 | _ctrls.text_box.add_color_region('/#', '#/', Color(.9, .6, 0)) 294 | _ctrls.text_box.add_color_region('/-', '-/', Color(1, 1, 0)) 295 | _ctrls.text_box.add_color_region('/*', '*/', Color(.5, .5, 1)) 296 | #_ctrls.text_box.set_symbol_color(Color(.5, .5, .5)) 297 | _ctrls.runtime_label.set_text('0.0') 298 | _ctrls.test_progress.set_max(1) 299 | 300 | #------------------------------------------------------------------------------- 301 | #------------------------------------------------------------------------------- 302 | func _input(event): 303 | #if the mouse is somewhere within the debug window 304 | if(_mouse_in): 305 | #Check for mouse click inside the resize handle 306 | if(event is InputEventMouseButton): 307 | if (event.button_index == 1): 308 | #It's checking a square area for the bottom right corner, but that's close enough. I'm lazy 309 | if(event.position.x > get_size().x + get_position().x - 10 and event.position.y > get_size().y + get_position().y - 10): 310 | if event.pressed: 311 | _mouse_down = true 312 | _mouse_down_pos = event.position 313 | else: 314 | _mouse_down = false 315 | #Reszie 316 | if(event is InputEventMouseMotion): 317 | if(_mouse_down): 318 | if(get_size() >= min_size): 319 | var new_size = get_size() + event.position - _mouse_down_pos 320 | var new_mouse_down_pos = event.position 321 | 322 | if(new_size.x < min_size.x): 323 | new_size.x = min_size.x 324 | new_mouse_down_pos.x = _mouse_down_pos.x 325 | 326 | if(new_size.y < min_size.y): 327 | new_size.y = min_size.y 328 | new_mouse_down_pos.y = _mouse_down_pos.y 329 | 330 | _mouse_down_pos = new_mouse_down_pos 331 | set_size(new_size) 332 | 333 | #------------------------------------------------------------------------------- 334 | #Custom drawing to indicate results. 335 | #------------------------------------------------------------------------------- 336 | func _draw(): 337 | #Draw the lines in the corner to show where you can 338 | #drag to resize the dialog 339 | var grab_margin = 2 340 | var line_space = 3 341 | var grab_line_color = Color(.4, .4, .4) 342 | for i in range(1, 6): 343 | draw_line(get_size() - Vector2(i * line_space, grab_margin), get_size() - Vector2(grab_margin, i * line_space), grab_line_color) 344 | 345 | return 346 | 347 | var where = Vector2(430, 565) 348 | var r = 25 349 | if(_summary.tests > 0): 350 | if(_summary.failed > 0): 351 | draw_circle(where, r , Color(1, 0, 0, 1)) 352 | else: 353 | draw_circle(where, r, Color(0, 1, 0, 1)) 354 | -------------------------------------------------------------------------------- /addons/gut/GutScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=2] 2 | 3 | [ext_resource path="res://addons/gut/GutScene.gd" type="Script" id=1] 4 | 5 | [sub_resource type="StyleBoxFlat" id=1] 6 | 7 | content_margin_left = -1.0 8 | content_margin_right = -1.0 9 | content_margin_top = -1.0 10 | content_margin_bottom = -1.0 11 | bg_color = Color( 0.193863, 0.205501, 0.214844, 1 ) 12 | draw_center = true 13 | border_width_left = 0 14 | border_width_top = 0 15 | border_width_right = 0 16 | border_width_bottom = 0 17 | border_color = Color( 0.8, 0.8, 0.8, 1 ) 18 | border_blend = false 19 | corner_radius_top_left = 20 20 | corner_radius_top_right = 20 21 | corner_radius_bottom_right = 0 22 | corner_radius_bottom_left = 0 23 | corner_detail = 8 24 | expand_margin_left = 0.0 25 | expand_margin_right = 0.0 26 | expand_margin_top = 0.0 27 | expand_margin_bottom = 0.0 28 | shadow_color = Color( 0, 0, 0, 0.6 ) 29 | shadow_size = 0 30 | anti_aliasing = true 31 | anti_aliasing_size = 1 32 | _sections_unfolded = [ "Corner Radius" ] 33 | 34 | [sub_resource type="StyleBoxFlat" id=2] 35 | 36 | content_margin_left = -1.0 37 | content_margin_right = -1.0 38 | content_margin_top = -1.0 39 | content_margin_bottom = -1.0 40 | bg_color = Color( 1, 1, 1, 1 ) 41 | draw_center = true 42 | border_width_left = 0 43 | border_width_top = 0 44 | border_width_right = 0 45 | border_width_bottom = 0 46 | border_color = Color( 0, 0, 0, 1 ) 47 | border_blend = false 48 | corner_radius_top_left = 5 49 | corner_radius_top_right = 5 50 | corner_radius_bottom_right = 0 51 | corner_radius_bottom_left = 0 52 | corner_detail = 8 53 | expand_margin_left = 0.0 54 | expand_margin_right = 0.0 55 | expand_margin_top = 0.0 56 | expand_margin_bottom = 0.0 57 | shadow_color = Color( 0, 0, 0, 0.6 ) 58 | shadow_size = 0 59 | anti_aliasing = true 60 | anti_aliasing_size = 1 61 | _sections_unfolded = [ "Border", "Corner Radius" ] 62 | 63 | [sub_resource type="Theme" id=3] 64 | 65 | resource_local_to_scene = true 66 | Panel/styles/panel = SubResource( 2 ) 67 | Panel/styles/panelf = null 68 | Panel/styles/panelnc = null 69 | _sections_unfolded = [ "Panel", "Panel/colors", "Panel/styles", "Resource" ] 70 | 71 | [node name="Gut" type="Panel" index="0"] 72 | 73 | anchor_left = 0.0 74 | anchor_top = 0.0 75 | anchor_right = 0.0 76 | anchor_bottom = 0.0 77 | margin_right = 740.0 78 | margin_bottom = 320.0 79 | rect_min_size = Vector2( 740, 250 ) 80 | rect_pivot_offset = Vector2( 0, 0 ) 81 | rect_clip_content = false 82 | mouse_filter = 0 83 | mouse_default_cursor_shape = 0 84 | size_flags_horizontal = 1 85 | size_flags_vertical = 1 86 | custom_styles/panel = SubResource( 1 ) 87 | script = ExtResource( 1 ) 88 | _sections_unfolded = [ "Theme", "Transform", "Z Index", "custom_styles" ] 89 | 90 | [node name="TitleBar" type="Panel" parent="." index="0"] 91 | 92 | editor/display_folded = true 93 | anchor_left = 0.0 94 | anchor_top = 0.0 95 | anchor_right = 1.0 96 | anchor_bottom = 0.0 97 | margin_bottom = 40.0 98 | rect_pivot_offset = Vector2( 0, 0 ) 99 | rect_clip_content = false 100 | mouse_filter = 0 101 | mouse_default_cursor_shape = 0 102 | size_flags_horizontal = 1 103 | size_flags_vertical = 1 104 | theme = SubResource( 3 ) 105 | _sections_unfolded = [ "Rect", "Theme" ] 106 | 107 | [node name="Title" type="Label" parent="TitleBar" index="0"] 108 | 109 | anchor_left = 0.0 110 | anchor_top = 0.0 111 | anchor_right = 1.0 112 | anchor_bottom = 0.0 113 | margin_bottom = 40.0 114 | rect_pivot_offset = Vector2( 0, 0 ) 115 | rect_clip_content = false 116 | mouse_filter = 2 117 | mouse_default_cursor_shape = 0 118 | size_flags_horizontal = 1 119 | size_flags_vertical = 4 120 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 121 | text = "Gut" 122 | align = 1 123 | valign = 1 124 | percent_visible = 1.0 125 | lines_skipped = 0 126 | max_lines_visible = -1 127 | _sections_unfolded = [ "Anchor", "custom_colors", "custom_fonts" ] 128 | 129 | [node name="Time" type="Label" parent="TitleBar" index="1"] 130 | 131 | anchor_left = 1.0 132 | anchor_top = 0.0 133 | anchor_right = 1.0 134 | anchor_bottom = 0.0 135 | margin_left = -114.0 136 | margin_right = -53.0 137 | margin_bottom = 40.0 138 | rect_pivot_offset = Vector2( 0, 0 ) 139 | rect_clip_content = false 140 | mouse_filter = 2 141 | mouse_default_cursor_shape = 0 142 | size_flags_horizontal = 1 143 | size_flags_vertical = 4 144 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 145 | text = "9999.99" 146 | valign = 1 147 | percent_visible = 1.0 148 | lines_skipped = 0 149 | max_lines_visible = -1 150 | _sections_unfolded = [ "Anchor", "custom_colors" ] 151 | 152 | [node name="Maximize" type="Button" parent="TitleBar" index="2"] 153 | 154 | anchor_left = 1.0 155 | anchor_top = 0.0 156 | anchor_right = 1.0 157 | anchor_bottom = 0.0 158 | margin_left = -30.0 159 | margin_top = 10.0 160 | margin_right = -6.0 161 | margin_bottom = 30.0 162 | rect_pivot_offset = Vector2( 0, 0 ) 163 | rect_clip_content = false 164 | focus_mode = 2 165 | mouse_filter = 0 166 | mouse_default_cursor_shape = 0 167 | size_flags_horizontal = 1 168 | size_flags_vertical = 1 169 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 170 | toggle_mode = false 171 | enabled_focus_mode = 2 172 | shortcut = null 173 | group = null 174 | text = "M" 175 | flat = true 176 | align = 1 177 | _sections_unfolded = [ "Anchor", "custom_colors" ] 178 | 179 | [node name="ScriptProgress" type="ProgressBar" parent="." index="1"] 180 | 181 | editor/display_folded = true 182 | anchor_left = 0.0 183 | anchor_top = 1.0 184 | anchor_right = 0.0 185 | anchor_bottom = 1.0 186 | margin_left = 70.0 187 | margin_top = -100.0 188 | margin_right = 180.0 189 | margin_bottom = -70.0 190 | rect_pivot_offset = Vector2( 0, 0 ) 191 | rect_clip_content = false 192 | mouse_filter = 0 193 | mouse_default_cursor_shape = 0 194 | size_flags_horizontal = 1 195 | size_flags_vertical = 0 196 | min_value = 0.0 197 | max_value = 100.0 198 | step = 1.0 199 | page = 0.0 200 | value = 0.0 201 | exp_edit = false 202 | rounded = false 203 | percent_visible = true 204 | 205 | [node name="Label" type="Label" parent="ScriptProgress" index="0"] 206 | 207 | anchor_left = 0.0 208 | anchor_top = 0.0 209 | anchor_right = 0.0 210 | anchor_bottom = 0.0 211 | margin_left = -70.0 212 | margin_right = -10.0 213 | margin_bottom = 24.0 214 | rect_pivot_offset = Vector2( 0, 0 ) 215 | rect_clip_content = false 216 | mouse_filter = 2 217 | mouse_default_cursor_shape = 0 218 | size_flags_horizontal = 1 219 | size_flags_vertical = 4 220 | text = "Script" 221 | align = 1 222 | valign = 1 223 | percent_visible = 1.0 224 | lines_skipped = 0 225 | max_lines_visible = -1 226 | 227 | [node name="TestProgress" type="ProgressBar" parent="." index="2"] 228 | 229 | editor/display_folded = true 230 | anchor_left = 0.0 231 | anchor_top = 1.0 232 | anchor_right = 0.0 233 | anchor_bottom = 1.0 234 | margin_left = 70.0 235 | margin_top = -70.0 236 | margin_right = 180.0 237 | margin_bottom = -40.0 238 | rect_pivot_offset = Vector2( 0, 0 ) 239 | rect_clip_content = false 240 | mouse_filter = 0 241 | mouse_default_cursor_shape = 0 242 | size_flags_horizontal = 1 243 | size_flags_vertical = 0 244 | min_value = 0.0 245 | max_value = 100.0 246 | step = 1.0 247 | page = 0.0 248 | value = 0.0 249 | exp_edit = false 250 | rounded = false 251 | percent_visible = true 252 | 253 | [node name="Label" type="Label" parent="TestProgress" index="0"] 254 | 255 | anchor_left = 0.0 256 | anchor_top = 0.0 257 | anchor_right = 0.0 258 | anchor_bottom = 0.0 259 | margin_left = -70.0 260 | margin_right = -10.0 261 | margin_bottom = 24.0 262 | rect_pivot_offset = Vector2( 0, 0 ) 263 | rect_clip_content = false 264 | mouse_filter = 2 265 | mouse_default_cursor_shape = 0 266 | size_flags_horizontal = 1 267 | size_flags_vertical = 4 268 | text = "Tests" 269 | align = 1 270 | valign = 1 271 | percent_visible = 1.0 272 | lines_skipped = 0 273 | max_lines_visible = -1 274 | 275 | [node name="TextDisplay" type="Panel" parent="." index="3"] 276 | 277 | editor/display_folded = true 278 | anchor_left = 0.0 279 | anchor_top = 0.0 280 | anchor_right = 1.0 281 | anchor_bottom = 1.0 282 | margin_top = 40.0 283 | margin_bottom = -107.0 284 | rect_pivot_offset = Vector2( 0, 0 ) 285 | rect_clip_content = false 286 | mouse_filter = 0 287 | mouse_default_cursor_shape = 0 288 | size_flags_horizontal = 1 289 | size_flags_vertical = 1 290 | _sections_unfolded = [ "Anchor", "Grow Direction", "Visibility" ] 291 | __meta__ = { 292 | "_edit_group_": true 293 | } 294 | 295 | [node name="RichTextLabel" type="TextEdit" parent="TextDisplay" index="0"] 296 | 297 | anchor_left = 0.0 298 | anchor_top = 0.0 299 | anchor_right = 1.0 300 | anchor_bottom = 1.0 301 | rect_pivot_offset = Vector2( 0, 0 ) 302 | rect_clip_content = false 303 | focus_mode = 2 304 | mouse_filter = 0 305 | mouse_default_cursor_shape = 0 306 | size_flags_horizontal = 1 307 | size_flags_vertical = 1 308 | text = "" 309 | readonly = true 310 | highlight_current_line = false 311 | syntax_highlighting = true 312 | show_line_numbers = false 313 | highlight_all_occurrences = false 314 | override_selected_font_color = false 315 | context_menu_enabled = true 316 | smooth_scrolling = true 317 | v_scroll_speed = 80.0 318 | hiding_enabled = 0 319 | wrap_lines = false 320 | caret_block_mode = false 321 | caret_blink = false 322 | caret_blink_speed = 0.65 323 | caret_moving_by_right_click = true 324 | _sections_unfolded = [ "Anchor", "Caret", "Grow Direction", "Margin", "Visibility", "custom_colors" ] 325 | 326 | [node name="FocusBlocker" type="Panel" parent="TextDisplay" index="1"] 327 | 328 | self_modulate = Color( 1, 1, 1, 0 ) 329 | anchor_left = 0.0 330 | anchor_top = 0.0 331 | anchor_right = 1.0 332 | anchor_bottom = 1.0 333 | margin_right = -10.0 334 | rect_pivot_offset = Vector2( 0, 0 ) 335 | rect_clip_content = false 336 | mouse_filter = 0 337 | mouse_default_cursor_shape = 0 338 | size_flags_horizontal = 1 339 | size_flags_vertical = 1 340 | _sections_unfolded = [ "Anchor", "Visibility" ] 341 | 342 | [node name="Navigation" type="Panel" parent="." index="4"] 343 | 344 | editor/display_folded = true 345 | self_modulate = Color( 1, 1, 1, 0 ) 346 | anchor_left = 0.0 347 | anchor_top = 1.0 348 | anchor_right = 0.0 349 | anchor_bottom = 1.0 350 | margin_left = 220.0 351 | margin_top = -100.0 352 | margin_right = 580.0 353 | rect_pivot_offset = Vector2( 0, 0 ) 354 | rect_clip_content = false 355 | mouse_filter = 0 356 | mouse_default_cursor_shape = 0 357 | size_flags_horizontal = 1 358 | size_flags_vertical = 1 359 | _sections_unfolded = [ "Visibility" ] 360 | 361 | [node name="Previous" type="Button" parent="Navigation" index="0"] 362 | 363 | anchor_left = 0.0 364 | anchor_top = 0.0 365 | anchor_right = 0.0 366 | anchor_bottom = 0.0 367 | margin_left = -30.0 368 | margin_right = 50.0 369 | margin_bottom = 40.0 370 | rect_pivot_offset = Vector2( 0, 0 ) 371 | rect_clip_content = false 372 | focus_mode = 2 373 | mouse_filter = 0 374 | mouse_default_cursor_shape = 0 375 | size_flags_horizontal = 1 376 | size_flags_vertical = 1 377 | toggle_mode = false 378 | enabled_focus_mode = 2 379 | shortcut = null 380 | group = null 381 | text = "<" 382 | flat = false 383 | align = 1 384 | 385 | [node name="Next" type="Button" parent="Navigation" index="1"] 386 | 387 | anchor_left = 0.0 388 | anchor_top = 0.0 389 | anchor_right = 0.0 390 | anchor_bottom = 0.0 391 | margin_left = 230.0 392 | margin_right = 310.0 393 | margin_bottom = 40.0 394 | rect_pivot_offset = Vector2( 0, 0 ) 395 | rect_clip_content = false 396 | focus_mode = 2 397 | mouse_filter = 0 398 | mouse_default_cursor_shape = 0 399 | size_flags_horizontal = 1 400 | size_flags_vertical = 1 401 | toggle_mode = false 402 | enabled_focus_mode = 2 403 | shortcut = null 404 | group = null 405 | text = ">" 406 | flat = false 407 | align = 1 408 | 409 | [node name="Run" type="Button" parent="Navigation" index="2"] 410 | 411 | anchor_left = 0.0 412 | anchor_top = 0.0 413 | anchor_right = 0.0 414 | anchor_bottom = 0.0 415 | margin_left = 60.0 416 | margin_right = 220.0 417 | margin_bottom = 40.0 418 | rect_pivot_offset = Vector2( 0, 0 ) 419 | rect_clip_content = false 420 | focus_mode = 2 421 | mouse_filter = 0 422 | mouse_default_cursor_shape = 0 423 | size_flags_horizontal = 1 424 | size_flags_vertical = 1 425 | toggle_mode = false 426 | enabled_focus_mode = 2 427 | shortcut = null 428 | group = null 429 | text = "Run" 430 | flat = false 431 | align = 1 432 | 433 | [node name="CurrentScript" type="Button" parent="Navigation" index="3"] 434 | 435 | anchor_left = 0.0 436 | anchor_top = 0.0 437 | anchor_right = 0.0 438 | anchor_bottom = 0.0 439 | margin_left = -30.0 440 | margin_top = 50.0 441 | margin_right = 310.0 442 | margin_bottom = 90.0 443 | rect_pivot_offset = Vector2( 0, 0 ) 444 | rect_clip_content = false 445 | focus_mode = 2 446 | mouse_filter = 0 447 | mouse_default_cursor_shape = 0 448 | size_flags_horizontal = 1 449 | size_flags_vertical = 1 450 | toggle_mode = false 451 | enabled_focus_mode = 2 452 | shortcut = null 453 | group = null 454 | text = "res://test/unit/test_gut.gd" 455 | flat = false 456 | clip_text = true 457 | align = 1 458 | 459 | [node name="ShowScripts" type="Button" parent="Navigation" index="4"] 460 | 461 | anchor_left = 0.0 462 | anchor_top = 0.0 463 | anchor_right = 0.0 464 | anchor_bottom = 0.0 465 | margin_left = 320.0 466 | margin_top = 50.0 467 | margin_right = 360.0 468 | margin_bottom = 90.0 469 | rect_pivot_offset = Vector2( 0, 0 ) 470 | rect_clip_content = false 471 | focus_mode = 2 472 | mouse_filter = 0 473 | mouse_default_cursor_shape = 0 474 | size_flags_horizontal = 1 475 | size_flags_vertical = 1 476 | toggle_mode = false 477 | enabled_focus_mode = 2 478 | shortcut = null 479 | group = null 480 | text = "..." 481 | flat = false 482 | align = 1 483 | 484 | [node name="LogLevelSlider" type="HSlider" parent="." index="5"] 485 | 486 | editor/display_folded = true 487 | anchor_left = 0.0 488 | anchor_top = 1.0 489 | anchor_right = 0.0 490 | anchor_bottom = 1.0 491 | margin_left = 80.0 492 | margin_top = -40.0 493 | margin_right = 130.0 494 | margin_bottom = -20.0 495 | rect_scale = Vector2( 2, 2 ) 496 | rect_pivot_offset = Vector2( 0, 0 ) 497 | rect_clip_content = false 498 | focus_mode = 2 499 | mouse_filter = 0 500 | mouse_default_cursor_shape = 0 501 | size_flags_horizontal = 1 502 | size_flags_vertical = 0 503 | min_value = 0.0 504 | max_value = 2.0 505 | step = 1.0 506 | page = 0.0 507 | value = 0.0 508 | exp_edit = false 509 | rounded = false 510 | editable = true 511 | tick_count = 3 512 | ticks_on_borders = true 513 | focus_mode = 2 514 | _sections_unfolded = [ "Rect" ] 515 | 516 | [node name="Label" type="Label" parent="LogLevelSlider" index="0"] 517 | 518 | anchor_left = 0.0 519 | anchor_top = 0.0 520 | anchor_right = 0.0 521 | anchor_bottom = 0.0 522 | margin_left = -35.0 523 | margin_top = 5.0 524 | margin_right = 25.0 525 | margin_bottom = 25.0 526 | rect_scale = Vector2( 0.5, 0.5 ) 527 | rect_pivot_offset = Vector2( 0, 0 ) 528 | rect_clip_content = false 529 | mouse_filter = 2 530 | mouse_default_cursor_shape = 0 531 | size_flags_horizontal = 1 532 | size_flags_vertical = 4 533 | text = "Log Level" 534 | align = 1 535 | valign = 1 536 | percent_visible = 1.0 537 | lines_skipped = 0 538 | max_lines_visible = -1 539 | _sections_unfolded = [ "Rect" ] 540 | 541 | [node name="ScriptsList" type="ItemList" parent="." index="6"] 542 | 543 | anchor_left = 0.0 544 | anchor_top = 0.0 545 | anchor_right = 0.0 546 | anchor_bottom = 1.0 547 | margin_left = 180.0 548 | margin_top = 40.0 549 | margin_right = 620.0 550 | margin_bottom = -108.0 551 | rect_pivot_offset = Vector2( 0, 0 ) 552 | rect_clip_content = true 553 | focus_mode = 2 554 | mouse_filter = 0 555 | mouse_default_cursor_shape = 0 556 | size_flags_horizontal = 1 557 | size_flags_vertical = 1 558 | items = [ ] 559 | select_mode = 0 560 | allow_reselect = true 561 | icon_mode = 1 562 | fixed_icon_size = Vector2( 0, 0 ) 563 | _sections_unfolded = [ "Anchor", "Columns", "Grow Direction", "Icon", "Margin", "Mouse", "Rect", "Size Flags", "Theme", "Visibility", "custom_colors", "custom_constants" ] 564 | 565 | [node name="ExtraOptions" type="Panel" parent="." index="7"] 566 | 567 | anchor_left = 1.0 568 | anchor_top = 1.0 569 | anchor_right = 1.0 570 | anchor_bottom = 1.0 571 | margin_left = -210.0 572 | margin_top = -246.0 573 | margin_bottom = -106.0 574 | rect_pivot_offset = Vector2( 0, 0 ) 575 | rect_clip_content = false 576 | mouse_filter = 0 577 | mouse_default_cursor_shape = 0 578 | size_flags_horizontal = 1 579 | size_flags_vertical = 1 580 | custom_styles/panel = SubResource( 1 ) 581 | _sections_unfolded = [ "Anchor", "Visibility" ] 582 | 583 | [node name="IgnorePause" type="CheckBox" parent="ExtraOptions" index="0"] 584 | 585 | anchor_left = 0.0 586 | anchor_top = 0.0 587 | anchor_right = 0.0 588 | anchor_bottom = 0.0 589 | margin_left = 10.0 590 | margin_top = 10.0 591 | margin_right = 128.0 592 | margin_bottom = 34.0 593 | rect_scale = Vector2( 1.5, 1.5 ) 594 | rect_pivot_offset = Vector2( 0, 0 ) 595 | rect_clip_content = false 596 | focus_mode = 2 597 | mouse_filter = 0 598 | mouse_default_cursor_shape = 0 599 | size_flags_horizontal = 1 600 | size_flags_vertical = 1 601 | toggle_mode = true 602 | enabled_focus_mode = 2 603 | shortcut = null 604 | group = null 605 | text = "Ignore Pauses" 606 | flat = false 607 | align = 0 608 | _sections_unfolded = [ "Rect" ] 609 | 610 | [node name="DisableBlocker" type="CheckBox" parent="ExtraOptions" index="1"] 611 | 612 | anchor_left = 0.0 613 | anchor_top = 0.0 614 | anchor_right = 0.0 615 | anchor_bottom = 0.0 616 | margin_left = 10.0 617 | margin_top = 50.0 618 | margin_right = 130.0 619 | margin_bottom = 74.0 620 | rect_scale = Vector2( 1.5, 1.5 ) 621 | rect_pivot_offset = Vector2( 0, 0 ) 622 | rect_clip_content = false 623 | focus_mode = 2 624 | mouse_filter = 0 625 | mouse_default_cursor_shape = 0 626 | size_flags_horizontal = 1 627 | size_flags_vertical = 1 628 | toggle_mode = true 629 | enabled_focus_mode = 2 630 | shortcut = null 631 | group = null 632 | text = "Selectable" 633 | flat = false 634 | align = 0 635 | _sections_unfolded = [ "Rect", "Size Flags" ] 636 | 637 | [node name="Copy" type="Button" parent="ExtraOptions" index="2"] 638 | 639 | anchor_left = 0.0 640 | anchor_top = 0.0 641 | anchor_right = 0.0 642 | anchor_bottom = 0.0 643 | margin_left = 20.0 644 | margin_top = 90.0 645 | margin_right = 200.0 646 | margin_bottom = 130.0 647 | rect_pivot_offset = Vector2( 0, 0 ) 648 | rect_clip_content = false 649 | focus_mode = 2 650 | mouse_filter = 0 651 | mouse_default_cursor_shape = 0 652 | size_flags_horizontal = 1 653 | size_flags_vertical = 1 654 | toggle_mode = false 655 | enabled_focus_mode = 2 656 | shortcut = null 657 | group = null 658 | text = "Copy" 659 | flat = false 660 | align = 1 661 | 662 | [node name="ResizeHandle" type="Control" parent="." index="8"] 663 | 664 | anchor_left = 1.0 665 | anchor_top = 1.0 666 | anchor_right = 1.0 667 | anchor_bottom = 1.0 668 | margin_left = -40.0 669 | margin_top = -40.0 670 | rect_pivot_offset = Vector2( 0, 0 ) 671 | rect_clip_content = false 672 | mouse_filter = 0 673 | mouse_default_cursor_shape = 0 674 | size_flags_horizontal = 1 675 | size_flags_vertical = 1 676 | _sections_unfolded = [ "Anchor", "Grow Direction", "Material", "Visibility" ] 677 | 678 | [node name="Continue" type="Panel" parent="." index="9"] 679 | 680 | self_modulate = Color( 1, 1, 1, 0 ) 681 | anchor_left = 1.0 682 | anchor_top = 1.0 683 | anchor_right = 1.0 684 | anchor_bottom = 1.0 685 | margin_left = -150.0 686 | margin_top = -100.0 687 | margin_right = -30.0 688 | margin_bottom = -10.0 689 | rect_pivot_offset = Vector2( 0, 0 ) 690 | rect_clip_content = false 691 | mouse_filter = 0 692 | mouse_default_cursor_shape = 0 693 | size_flags_horizontal = 1 694 | size_flags_vertical = 1 695 | _sections_unfolded = [ "Anchor", "Visibility" ] 696 | 697 | [node name="Continue" type="Button" parent="Continue" index="0"] 698 | 699 | anchor_left = 0.0 700 | anchor_top = 0.0 701 | anchor_right = 0.0 702 | anchor_bottom = 0.0 703 | margin_top = 50.0 704 | margin_right = 119.0 705 | margin_bottom = 90.0 706 | rect_pivot_offset = Vector2( 0, 0 ) 707 | rect_clip_content = false 708 | focus_mode = 2 709 | mouse_filter = 0 710 | mouse_default_cursor_shape = 0 711 | size_flags_horizontal = 1 712 | size_flags_vertical = 1 713 | disabled = true 714 | toggle_mode = false 715 | enabled_focus_mode = 2 716 | shortcut = null 717 | group = null 718 | text = "Continue" 719 | flat = false 720 | align = 1 721 | 722 | [node name="ShowExtras" type="Button" parent="Continue" index="1"] 723 | 724 | anchor_left = 0.0 725 | anchor_top = 0.0 726 | anchor_right = 0.0 727 | anchor_bottom = 0.0 728 | margin_left = 50.0 729 | margin_right = 120.0 730 | margin_bottom = 40.0 731 | rect_pivot_offset = Vector2( 35, 20 ) 732 | rect_clip_content = false 733 | focus_mode = 2 734 | mouse_filter = 0 735 | mouse_default_cursor_shape = 0 736 | size_flags_horizontal = 1 737 | size_flags_vertical = 1 738 | toggle_mode = true 739 | enabled_focus_mode = 2 740 | shortcut = null 741 | group = null 742 | text = "_" 743 | flat = false 744 | align = 1 745 | _sections_unfolded = [ "Rect" ] 746 | 747 | [node name="Summary" type="Node2D" parent="." index="10"] 748 | 749 | editor/display_folded = true 750 | position = Vector2( 0, 3 ) 751 | _sections_unfolded = [ "Transform" ] 752 | 753 | [node name="Passing" type="Label" parent="Summary" index="0"] 754 | 755 | anchor_left = 0.0 756 | anchor_top = 0.0 757 | anchor_right = 0.0 758 | anchor_bottom = 0.0 759 | margin_top = 10.0 760 | margin_right = 40.0 761 | margin_bottom = 24.0 762 | rect_pivot_offset = Vector2( 0, 0 ) 763 | rect_clip_content = false 764 | mouse_filter = 2 765 | mouse_default_cursor_shape = 0 766 | size_flags_horizontal = 1 767 | size_flags_vertical = 4 768 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 769 | text = "0" 770 | align = 1 771 | valign = 1 772 | percent_visible = 1.0 773 | lines_skipped = 0 774 | max_lines_visible = -1 775 | 776 | [node name="Failing" type="Label" parent="Summary" index="1"] 777 | 778 | anchor_left = 0.0 779 | anchor_top = 0.0 780 | anchor_right = 0.0 781 | anchor_bottom = 0.0 782 | margin_left = 40.0 783 | margin_top = 10.0 784 | margin_right = 80.0 785 | margin_bottom = 24.0 786 | rect_pivot_offset = Vector2( 0, 0 ) 787 | rect_clip_content = false 788 | mouse_filter = 2 789 | mouse_default_cursor_shape = 0 790 | size_flags_horizontal = 1 791 | size_flags_vertical = 4 792 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 793 | text = "0" 794 | align = 1 795 | valign = 1 796 | percent_visible = 1.0 797 | lines_skipped = 0 798 | max_lines_visible = -1 799 | 800 | [connection signal="mouse_entered" from="TitleBar" to="." method="_on_TitleBar_mouse_entered"] 801 | 802 | [connection signal="mouse_exited" from="TitleBar" to="." method="_on_TitleBar_mouse_exited"] 803 | 804 | [connection signal="draw" from="TitleBar/Maximize" to="." method="_on_Maximize_draw"] 805 | 806 | [connection signal="pressed" from="TitleBar/Maximize" to="." method="_on_Maximize_pressed"] 807 | 808 | [connection signal="gui_input" from="TextDisplay/RichTextLabel" to="." method="_on_RichTextLabel_gui_input"] 809 | 810 | [connection signal="gui_input" from="TextDisplay/FocusBlocker" to="." method="_on_FocusBlocker_gui_input"] 811 | 812 | [connection signal="pressed" from="Navigation/Previous" to="." method="_on_Previous_pressed"] 813 | 814 | [connection signal="pressed" from="Navigation/Next" to="." method="_on_Next_pressed"] 815 | 816 | [connection signal="pressed" from="Navigation/Run" to="." method="_on_Run_pressed"] 817 | 818 | [connection signal="pressed" from="Navigation/CurrentScript" to="." method="_on_CurrentScript_pressed"] 819 | 820 | [connection signal="pressed" from="Navigation/ShowScripts" to="." method="_on_ShowScripts_pressed"] 821 | 822 | [connection signal="value_changed" from="LogLevelSlider" to="." method="_on_LogLevelSlider_value_changed"] 823 | 824 | [connection signal="item_selected" from="ScriptsList" to="." method="_on_ScriptsList_item_selected"] 825 | 826 | [connection signal="pressed" from="ExtraOptions/IgnorePause" to="." method="_on_IgnorePause_pressed"] 827 | 828 | [connection signal="toggled" from="ExtraOptions/DisableBlocker" to="." method="_on_DisableBlocker_toggled"] 829 | 830 | [connection signal="pressed" from="ExtraOptions/Copy" to="." method="_on_Copy_pressed"] 831 | 832 | [connection signal="mouse_entered" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_entered"] 833 | 834 | [connection signal="mouse_exited" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_exited"] 835 | 836 | [connection signal="pressed" from="Continue/Continue" to="." method="_on_Continue_pressed"] 837 | 838 | [connection signal="draw" from="Continue/ShowExtras" to="." method="_on_ShowExtras_draw"] 839 | 840 | [connection signal="toggled" from="Continue/ShowExtras" to="." method="_on_ShowExtras_toggled"] 841 | 842 | 843 | --------------------------------------------------------------------------------