├── Levels ├── 1.txt ├── 5.txt ├── 6.txt ├── 2.txt ├── 9.txt ├── 3.txt ├── 7.txt ├── 4.txt ├── 8.txt └── 10.txt ├── Tile.psd ├── screenshot.png ├── SFX ├── pickUpTile.ogg ├── placeTile.ogg ├── rotateTile.ogg ├── whooshBack.ogg ├── circuitFail.ogg ├── circuitComplete.ogg ├── placeTile.ogg.import ├── pickUpTile.ogg.import ├── rotateTile.ogg.import ├── whooshBack.ogg.import ├── circuitFail.ogg.import └── circuitComplete.ogg.import ├── Textures ├── Icon.png ├── Tile.png ├── line.png ├── stop.png ├── Cursor.png ├── Splash.png ├── largeCurve.png ├── smallCurve.png ├── BoardGradient.png ├── old thin lines │ ├── line_thin.png │ ├── stop_thin.png │ ├── largeCurve_thin.png │ ├── smallCurve_thin.png │ ├── line_thin.png.import │ ├── stop_thin.png.import │ ├── largeCurve_thin.png.import │ └── smallCurve_thin.png.import ├── Icon.png.import ├── Tile.png.import ├── line.png.import ├── stop.png.import ├── Cursor.png.import ├── Splash.png.import ├── HexTile.png.import ├── TileBoard.png.import ├── TileCursor.png.import ├── largeCurve.png.import ├── smallCurve.png.import ├── BoardGradient.png.import ├── WhiteGradientTile.png.import ├── HexTileUnsaturated.png.import ├── TileBoardUnsaturated.png.import └── TileCursorUnsaturated.png.import ├── default_bus_layout.tres ├── Music ├── 2_bass_loop.ogg ├── 1_melody_loop.ogg ├── 3_swells_loop.ogg ├── 5_eighths_loop.ogg ├── 4_decoration_loop.ogg ├── 2_bass_loop.ogg.import ├── 1_melody_loop.ogg.import ├── 3_swells_loop.ogg.import ├── 5_eighths_loop.ogg.import └── 4_decoration_loop.ogg.import ├── .gitignore ├── Puzzle.tscn ├── Splash.tscn ├── default_env.tres ├── Code ├── CellInfo.cs ├── Splash.cs ├── Resources.cs ├── HexMap.cs ├── Puzzle.cs ├── PuzzleTileHex.cs └── HexCoord.cs ├── mapformat.txt ├── Properties └── AssemblyInfo.cs ├── Circuitous Circuits.sln ├── README.md ├── project.godot └── Circuitous Circuits.csproj /Levels/1.txt: -------------------------------------------------------------------------------- 1 | 2 2 | 1,3|345012 -------------------------------------------------------------------------------- /Levels/5.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 3,3|345012 -------------------------------------------------------------------------------- /Levels/6.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 3,3|012345 -------------------------------------------------------------------------------- /Levels/2.txt: -------------------------------------------------------------------------------- 1 | 3|2DA5C9||AC7CE8||FF5446 2 | 2,3|521430 -------------------------------------------------------------------------------- /Levels/9.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 2,3|315042 3 | 5,2|012345 4 | 3,5|453201 -------------------------------------------------------------------------------- /Levels/3.txt: -------------------------------------------------------------------------------- 1 | 3|A046CC||7764FF||FFC714 2 | 1,3|421503 3 | 3,3|105432 -------------------------------------------------------------------------------- /Levels/7.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 1,3|514320 3 | 4,1|315042 4 | 5,3|321045 5 | 2,5|532140 -------------------------------------------------------------------------------- /Tile.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Tile.psd -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/screenshot.png -------------------------------------------------------------------------------- /SFX/pickUpTile.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/pickUpTile.ogg -------------------------------------------------------------------------------- /SFX/placeTile.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/placeTile.ogg -------------------------------------------------------------------------------- /SFX/rotateTile.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/rotateTile.ogg -------------------------------------------------------------------------------- /SFX/whooshBack.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/whooshBack.ogg -------------------------------------------------------------------------------- /Textures/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/Icon.png -------------------------------------------------------------------------------- /Textures/Tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/Tile.png -------------------------------------------------------------------------------- /Textures/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/line.png -------------------------------------------------------------------------------- /Textures/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/stop.png -------------------------------------------------------------------------------- /default_bus_layout.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="AudioBusLayout" format=2] 2 | 3 | [resource] 4 | 5 | -------------------------------------------------------------------------------- /Music/2_bass_loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Music/2_bass_loop.ogg -------------------------------------------------------------------------------- /SFX/circuitFail.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/circuitFail.ogg -------------------------------------------------------------------------------- /Textures/Cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/Cursor.png -------------------------------------------------------------------------------- /Textures/Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/Splash.png -------------------------------------------------------------------------------- /Levels/4.txt: -------------------------------------------------------------------------------- 1 | 3|64E1FF|349AB2|FF7EAE||FF4500 2 | 0,3|421503 3 | 3,1|435102 4 | 4,3|250431 5 | 1,5|435102 -------------------------------------------------------------------------------- /Music/1_melody_loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Music/1_melody_loop.ogg -------------------------------------------------------------------------------- /Music/3_swells_loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Music/3_swells_loop.ogg -------------------------------------------------------------------------------- /SFX/circuitComplete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/SFX/circuitComplete.ogg -------------------------------------------------------------------------------- /Textures/largeCurve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/largeCurve.png -------------------------------------------------------------------------------- /Textures/smallCurve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/smallCurve.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .import/ 2 | export.cfg 3 | export_presets.cfg 4 | .mono/ 5 | .vs/ 6 | .DS_Store 7 | mono_crash* 8 | -------------------------------------------------------------------------------- /Music/5_eighths_loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Music/5_eighths_loop.ogg -------------------------------------------------------------------------------- /Textures/BoardGradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/BoardGradient.png -------------------------------------------------------------------------------- /Music/4_decoration_loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Music/4_decoration_loop.ogg -------------------------------------------------------------------------------- /Levels/8.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 0,3|012543 3 | 3,0|412305 4 | 6,0|052341 5 | 6,3|210345 6 | 3,6|032145 7 | 0,6|014325 8 | 3,3|103254 -------------------------------------------------------------------------------- /Textures/old thin lines/line_thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/old thin lines/line_thin.png -------------------------------------------------------------------------------- /Textures/old thin lines/stop_thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/old thin lines/stop_thin.png -------------------------------------------------------------------------------- /Textures/old thin lines/largeCurve_thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/old thin lines/largeCurve_thin.png -------------------------------------------------------------------------------- /Textures/old thin lines/smallCurve_thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhutch/CircuitousCircuits/HEAD/Textures/old thin lines/smallCurve_thin.png -------------------------------------------------------------------------------- /Puzzle.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://Code/Puzzle.cs" type="Script" id=1] 4 | 5 | [node name="Puzzle" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | -------------------------------------------------------------------------------- /Splash.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://Code/Splash.cs" type="Script" id=1] 4 | 5 | [node name="Splash" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | 9 | -------------------------------------------------------------------------------- /Levels/10.txt: -------------------------------------------------------------------------------- 1 | 4 2 | 0,6|534120 3 | 0,3|015432 4 | 3,0|435102 5 | 6,0|102345 6 | 6,3|240513 7 | 3,6|321054 8 | 1,4|103254 9 | 2,2|345012 10 | 4,1|453201 11 | 5,2|043215 12 | 4,4|315042 13 | 3,3|210435 -------------------------------------------------------------------------------- /Code/CellInfo.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using Settworks.Hexagons; 3 | 4 | public struct CellInfo 5 | { 6 | public CellInfo(HexCoord coord, Node2D tile) 7 | { 8 | Coord = coord; 9 | Tile = tile; 10 | } 11 | 12 | public HexCoord Coord { get; } 13 | public Node2D Tile { get; } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /mapformat.txt: -------------------------------------------------------------------------------- 1 | map is a text file 2 | 3 | first line is grid size (edge length) e.g. 4 | 4 5 | 6 | each following line is a tile position and description 7 | e.g. 8 | 0,3|015432 9 | 10 | hexes are pointy top, zero is bottom left 11 | 12 | blank cell simply has no description, only location, e.g. 13 | 4,1| 14 | 15 | levels are numbered in order e.g. 1.txt, 2.txt -------------------------------------------------------------------------------- /SFX/placeTile.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/placeTile.ogg-b5d05f19754bddba46059242976e8593.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/placeTile.ogg" 10 | dest_files=[ "res://.import/placeTile.ogg-b5d05f19754bddba46059242976e8593.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /SFX/pickUpTile.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/pickUpTile.ogg-6189d34fcef28df501ed0eb942317cd3.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/pickUpTile.ogg" 10 | dest_files=[ "res://.import/pickUpTile.ogg-6189d34fcef28df501ed0eb942317cd3.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /SFX/rotateTile.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/rotateTile.ogg-687fe59aa646014d96a24139f124ebb1.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/rotateTile.ogg" 10 | dest_files=[ "res://.import/rotateTile.ogg-687fe59aa646014d96a24139f124ebb1.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /SFX/whooshBack.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/whooshBack.ogg-a9a8c8998839f373ebb8cd14d8990c6a.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/whooshBack.ogg" 10 | dest_files=[ "res://.import/whooshBack.ogg-a9a8c8998839f373ebb8cd14d8990c6a.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /SFX/circuitFail.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/circuitFail.ogg-a7be8db6d0a2ffb432da321e56de27b7.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/circuitFail.ogg" 10 | dest_files=[ "res://.import/circuitFail.ogg-a7be8db6d0a2ffb432da321e56de27b7.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Music/2_bass_loop.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/2_bass_loop.ogg-85f90bfaf2c37c33b56eefa35663dcd9.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://Music/2_bass_loop.ogg" 10 | dest_files=[ "res://.import/2_bass_loop.ogg-85f90bfaf2c37c33b56eefa35663dcd9.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Music/1_melody_loop.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/1_melody_loop.ogg-806e057ea0d3f4cce3fa5d5243568cc1.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://Music/1_melody_loop.ogg" 10 | dest_files=[ "res://.import/1_melody_loop.ogg-806e057ea0d3f4cce3fa5d5243568cc1.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Music/3_swells_loop.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/3_swells_loop.ogg-f80eb3e90f3abf958f2ddef73254f297.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://Music/3_swells_loop.ogg" 10 | dest_files=[ "res://.import/3_swells_loop.ogg-f80eb3e90f3abf958f2ddef73254f297.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Music/5_eighths_loop.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/5_eighths_loop.ogg-d5f74ba4083b2e3143efafd5337e7211.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://Music/5_eighths_loop.ogg" 10 | dest_files=[ "res://.import/5_eighths_loop.ogg-d5f74ba4083b2e3143efafd5337e7211.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /SFX/circuitComplete.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/circuitComplete.ogg-037d5497a8e6ce445c9b0effa4457c5f.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://SFX/circuitComplete.ogg" 10 | dest_files=[ "res://.import/circuitComplete.ogg-037d5497a8e6ce445c9b0effa4457c5f.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Music/4_decoration_loop.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="ogg_vorbis" 4 | type="AudioStreamOGGVorbis" 5 | path="res://.import/4_decoration_loop.ogg-84c3a2aa716eec28f0746932075a7a74.oggstr" 6 | 7 | [deps] 8 | 9 | source_file="res://Music/4_decoration_loop.ogg" 10 | dest_files=[ "res://.import/4_decoration_loop.ogg-84c3a2aa716eec28f0746932075a7a74.oggstr" ] 11 | 12 | [params] 13 | 14 | loop=false 15 | loop_offset=0 16 | -------------------------------------------------------------------------------- /Textures/Icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/Icon.png-bc77f958016787d7e72c5230e0787ef1.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/Icon.png" 13 | dest_files=[ "res://.import/Icon.png-bc77f958016787d7e72c5230e0787ef1.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 | -------------------------------------------------------------------------------- /Textures/Tile.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/Tile.png-a3076d311360f565b2fc6ab3f16b8e8b.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/Tile.png" 13 | dest_files=[ "res://.import/Tile.png-a3076d311360f565b2fc6ab3f16b8e8b.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 | -------------------------------------------------------------------------------- /Textures/line.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/line.png-178f4c8616dd92900f3016228359bdbc.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/line.png" 13 | dest_files=[ "res://.import/line.png-178f4c8616dd92900f3016228359bdbc.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 | -------------------------------------------------------------------------------- /Textures/stop.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/stop.png-7eac3ff8ffddf7297d790669dbaa0921.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/stop.png" 13 | dest_files=[ "res://.import/stop.png-7eac3ff8ffddf7297d790669dbaa0921.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 | -------------------------------------------------------------------------------- /Textures/Cursor.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/Cursor.png-7eb9f6503660d8c9c1f3c590b8672743.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/Cursor.png" 13 | dest_files=[ "res://.import/Cursor.png-7eb9f6503660d8c9c1f3c590b8672743.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 | -------------------------------------------------------------------------------- /Textures/Splash.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/Splash.png-9199200df1a3090c717978b4d1fb42b0.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/Splash.png" 13 | dest_files=[ "res://.import/Splash.png-9199200df1a3090c717978b4d1fb42b0.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 | -------------------------------------------------------------------------------- /Textures/HexTile.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/HexTile.png-fb483fe522d1e520005f499da498e7a0.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/HexTile.png" 13 | dest_files=[ "res://.import/HexTile.png-fb483fe522d1e520005f499da498e7a0.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 | -------------------------------------------------------------------------------- /Textures/TileBoard.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/TileBoard.png-32acc6cdd5519210e8fea18e87bdd2cc.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/TileBoard.png" 13 | dest_files=[ "res://.import/TileBoard.png-32acc6cdd5519210e8fea18e87bdd2cc.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 | -------------------------------------------------------------------------------- /Textures/TileCursor.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/TileCursor.png-7cf066fc66ffd211c4c788f1b5eb9924.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/TileCursor.png" 13 | dest_files=[ "res://.import/TileCursor.png-7cf066fc66ffd211c4c788f1b5eb9924.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 | -------------------------------------------------------------------------------- /Textures/largeCurve.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/largeCurve.png-43d4b684f648de19e5b9739fe0cf71d2.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/largeCurve.png" 13 | dest_files=[ "res://.import/largeCurve.png-43d4b684f648de19e5b9739fe0cf71d2.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 | -------------------------------------------------------------------------------- /Textures/smallCurve.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/smallCurve.png-a316c3b082fafefd20e1170fcc236176.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/smallCurve.png" 13 | dest_files=[ "res://.import/smallCurve.png-a316c3b082fafefd20e1170fcc236176.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 | -------------------------------------------------------------------------------- /Textures/BoardGradient.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/BoardGradient.png-9eb77d61fd6985f37db47d393172a9d5.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/BoardGradient.png" 13 | dest_files=[ "res://.import/BoardGradient.png-9eb77d61fd6985f37db47d393172a9d5.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 | -------------------------------------------------------------------------------- /Textures/WhiteGradientTile.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/WhiteGradientTile.png-f4955e35068c17105308ac9dac6f2160.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/WhiteGradientTile.png" 13 | dest_files=[ "res://.import/WhiteGradientTile.png-f4955e35068c17105308ac9dac6f2160.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 | -------------------------------------------------------------------------------- /Textures/old thin lines/line_thin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/line_thin.png-45f566ae0113d9526c24f59a020206ee.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/old thin lines/line_thin.png" 13 | dest_files=[ "res://.import/line_thin.png-45f566ae0113d9526c24f59a020206ee.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 | -------------------------------------------------------------------------------- /Textures/old thin lines/stop_thin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/stop_thin.png-86ab42c8c442cfad4362cffcb9a9e329.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/old thin lines/stop_thin.png" 13 | dest_files=[ "res://.import/stop_thin.png-86ab42c8c442cfad4362cffcb9a9e329.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 | -------------------------------------------------------------------------------- /Textures/HexTileUnsaturated.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/HexTileUnsaturated.png-d3cd79265890437d86a45bdda63cf30e.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/HexTileUnsaturated.png" 13 | dest_files=[ "res://.import/HexTileUnsaturated.png-d3cd79265890437d86a45bdda63cf30e.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 | -------------------------------------------------------------------------------- /Textures/TileBoardUnsaturated.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/TileBoardUnsaturated.png-f12125055ee16f3e06ebc89f62f1b0ce.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/TileBoardUnsaturated.png" 13 | dest_files=[ "res://.import/TileBoardUnsaturated.png-f12125055ee16f3e06ebc89f62f1b0ce.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 | -------------------------------------------------------------------------------- /Textures/TileCursorUnsaturated.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/TileCursorUnsaturated.png-1e4f0d72918f2b0f548022d86b10e5b1.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/TileCursorUnsaturated.png" 13 | dest_files=[ "res://.import/TileCursorUnsaturated.png-1e4f0d72918f2b0f548022d86b10e5b1.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 | -------------------------------------------------------------------------------- /Textures/old thin lines/largeCurve_thin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/largeCurve_thin.png-cd2762722969f53be7db03011141cdbc.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/old thin lines/largeCurve_thin.png" 13 | dest_files=[ "res://.import/largeCurve_thin.png-cd2762722969f53be7db03011141cdbc.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 | -------------------------------------------------------------------------------- /Textures/old thin lines/smallCurve_thin.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/smallCurve_thin.png-db45283b435149e105de454a47a9c5f5.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Textures/old thin lines/smallCurve_thin.png" 13 | dest_files=[ "res://.import/smallCurve_thin.png-db45283b435149e105de454a47a9c5f5.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 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | // Information about this assembly is defined by the following attributes. 4 | // Change them to the values specific to your project. 5 | 6 | [assembly: AssemblyTitle("Circuitous Circuits")] 7 | [assembly: AssemblyDescription("")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("")] 11 | [assembly: AssemblyCopyright("")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 16 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 17 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 18 | 19 | [assembly: AssemblyVersion("1.0.*")] 20 | 21 | // The following attributes are used to specify the signing key for the assembly, 22 | // if desired. See the Mono documentation for more information about signing. 23 | 24 | //[assembly: AssemblyDelaySign(false)] 25 | //[assembly: AssemblyKeyFile("")] 26 | -------------------------------------------------------------------------------- /Circuitous Circuits.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2012 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Circuitous Circuits", "Circuitous Circuits.csproj", "{2B950B54-DDB8-4BE4-90D8-89C0B443E481}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | Tools|Any CPU = Tools|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Release|Any CPU.Build.0 = Release|Any CPU 16 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Tools|Any CPU.ActiveCfg = Tools|Any CPU 17 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481}.Tools|Any CPU.Build.0 = Tools|Any CPU 18 | EndGlobalSection 19 | EndGlobal 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circuitous Circuits 2 | 3 | A [Train Jam 2019](https://itch.io/jam/train-jam-2019) game by Mikayla Hutchinson([@mjhutchinson](https://twitter.com/mjhutchinson)) 4 | and Candy Emberley ([@CandyComposer](https://twitter.com/CandyComposer)). 5 | 6 | Make the longest circuit that meets up with itself - a circuitous circuit! 7 | 8 | ![Screenshot of the game](screenshot.png) 9 | 10 | Binaries are available [on itch.io](https://mhutch.itch.io/circuitous-circuits). 11 | 12 | NOTE: we didn't figure out how to measure or incentivize the *longest* circuit 13 | so currently *any* circuit is enough to advance to the next level. 14 | 15 | This game was made with [the Godot engine](https://godotengine.org/). 16 | 17 | ## License 18 | 19 | * Code by Mikayla Hutchinson, licensed under [the MIT license](https://opensource.org/licenses/MIT) 20 | * Music and sound by Candy Emberley, licensed under [the CC-BY-4.0 license](https://creativecommons.org/licenses/by/4.0/) 21 | * Art by Candy Emberley and Mikayla Hutchinson, licensed under [the CC-0 license](https://creativecommons.org/share-your-work/public-domain/cc0/) -------------------------------------------------------------------------------- /Code/Splash.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | 4 | public class Splash : Node2D 5 | { 6 | Sprite splashImg; 7 | 8 | public override void _Ready() 9 | { 10 | splashImg = new Sprite(); 11 | splashImg.Texture = Resources.Textures.Splash; 12 | AddChild(splashImg); 13 | 14 | GetViewport().Connect("size_changed", this, nameof(Rescale)); 15 | Rescale(); 16 | } 17 | 18 | public override void _Process(float delta) 19 | { 20 | if (Input.IsActionJustPressed("fullscreen")) 21 | { 22 | OS.WindowFullscreen = !OS.WindowFullscreen; 23 | } 24 | else if (Input.IsActionJustPressed("start")) 25 | { 26 | GetTree().ChangeScene("res://Puzzle.tscn");//.Quit(); 27 | } 28 | else if (Input.IsActionJustPressed("back")) 29 | { 30 | GetTree().Quit(); 31 | } 32 | } 33 | 34 | public void Rescale() 35 | { 36 | var rect = GetViewportRect(); 37 | var scale = rect.Size.y/splashImg.Texture.GetHeight(); 38 | splashImg.Scale = new Vector2(scale, scale); 39 | splashImg.Position = new Vector2(rect.Size.x / 2f, rect.Size.y / 2f); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Code/Resources.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Godot; 3 | 4 | //GODOT: could we autogenerate these, or at least constants for the IDs 5 | public static class Resources 6 | { 7 | public static class Textures 8 | { 9 | public static Texture Tile = GD.Load("res://Textures/Tile.png"); 10 | public static Texture Cursor = GD.Load("res://Textures/Cursor.png"); 11 | public static Texture BoardGradient = GD.Load("res://Textures/BoardGradient.png"); 12 | public static Texture CurveLarge = GD.Load("res://Textures/largeCurve.png"); 13 | public static Texture CurveSmall = GD.Load("res://Textures/smallCurve.png"); 14 | public static Texture Line = GD.Load("res://Textures/line.png"); 15 | public static Texture Stop = GD.Load("res://Textures/stop.png"); 16 | public static Texture Splash = GD.Load("res://Textures/Splash.png"); 17 | } 18 | 19 | public static class Music 20 | { 21 | public static AudioStreamOGGVorbis Melody = GD.Load("res://Music/1_melody_loop.ogg"); 22 | public static AudioStreamOGGVorbis Bass = GD.Load("res://Music/2_bass_loop.ogg"); 23 | public static AudioStreamOGGVorbis Swells = GD.Load("res://Music/3_swells_loop.ogg"); 24 | public static AudioStreamOGGVorbis Decoration = GD.Load("res://Music/4_decoration_loop.ogg"); 25 | public static AudioStreamOGGVorbis Eighths = GD.Load("res://Music/5_eighths_loop.ogg"); 26 | } 27 | 28 | public static class Sfx 29 | { 30 | public static AudioStreamOGGVorbis CircuitComplete = GD.Load("res://SFX/circuitComplete.ogg"); 31 | public static AudioStreamOGGVorbis CircuitFail = GD.Load("res://SFX/circuitFail.ogg"); 32 | public static AudioStreamOGGVorbis PickUpTile = GD.Load("res://SFX/pickUpTile.ogg"); 33 | public static AudioStreamOGGVorbis PlaceTile = GD.Load("res://SFX/placeTile.ogg"); 34 | public static AudioStreamOGGVorbis RotateTile = GD.Load("res://SFX/rotateTile.ogg"); 35 | public static AudioStreamOGGVorbis WhooshBack = GD.Load("res://SFX/whooshBack.ogg"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Code/HexMap.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using Settworks.Hexagons; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public class HexMap 7 | { 8 | CellInfo[][] map; 9 | 10 | public HexMap(int edgeSize) 11 | { 12 | int size = edgeSize * 2 - 1; 13 | map = new CellInfo[size][]; 14 | 15 | for (int r = 0; r < size; r++) 16 | { 17 | map[r] = (new CellInfo[size - Math.Abs(edgeSize - 1 - r)]); 18 | } 19 | 20 | int cellCount = 0; 21 | for (int i = edgeSize - 1; i > 0; i--) 22 | { 23 | cellCount += i; 24 | } 25 | CellCount = cellCount * 6 + 1; 26 | } 27 | 28 | public void Initialize(Func creator) 29 | { 30 | int edgeSize = (map.Length + 1) / 2; 31 | for (int r = 0; r < map.Length; r++) 32 | { 33 | var arr = map[r]; 34 | int offset = -Math.Max(0, edgeSize - 1 - r); 35 | for (int j = 0; j < arr.Length; j++) 36 | { 37 | arr[j] = creator(new HexCoord(j - offset, r)); 38 | } 39 | } 40 | } 41 | 42 | public void SetCell(CellInfo info) 43 | { 44 | var c = info.Coord; 45 | var row = map[c.r]; 46 | int edgeSize = (map.Length + 1) / 2; 47 | var index = c.q - Math.Max(0, edgeSize - 1 - c.r); 48 | row[index] = info; 49 | 50 | if (info.Tile is PuzzleTileHex) 51 | { 52 | TileCount++; 53 | } 54 | } 55 | 56 | public CellInfo? TryGetCell(HexCoord c) 57 | { 58 | if (c.r >= 0 && c.r < map.Length) 59 | { 60 | var row = map[c.r]; 61 | int edgeSize = (map.Length + 1) / 2; 62 | var index = c.q - Math.Max(0, edgeSize - 1 - c.r); 63 | if (index >= 0 && index < row.Length) 64 | { 65 | return row[index]; 66 | } 67 | } 68 | return null; 69 | } 70 | 71 | public IEnumerable GetAllTiles() where T : Node2D 72 | { 73 | for (int r = 0; r < map.Length; r++) 74 | { 75 | var arr = map[r]; 76 | for (int j = 0; j < arr.Length; j++) 77 | { 78 | if (arr[j].Tile is T t) 79 | { 80 | yield return t; 81 | } 82 | } 83 | } 84 | } 85 | 86 | public int Size => map.Length + 1; 87 | public int EdgeSize => (map.Length + 1) / 2; 88 | public int CellCount { get; } 89 | public int TileCount { get; private set; } 90 | } -------------------------------------------------------------------------------- /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="Circuitous Circuits" 19 | run/main_scene="res://Splash.tscn" 20 | config/icon="res://Textures/Icon.png" 21 | 22 | [display] 23 | 24 | window/size/fullscreen=true 25 | 26 | [input] 27 | 28 | left_click={ 29 | "deadzone": 0.5, 30 | "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null) 31 | ] 32 | } 33 | rotate_left={ 34 | "deadzone": 0.5, 35 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) 36 | ] 37 | } 38 | rotate_right={ 39 | "deadzone": 0.5, 40 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) 41 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) 42 | ] 43 | } 44 | back={ 45 | "deadzone": 0.5, 46 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777217,"unicode":0,"echo":false,"script":null) 47 | ] 48 | } 49 | reset={ 50 | "deadzone": 0.5, 51 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":82,"unicode":0,"echo":false,"script":null) 52 | ] 53 | } 54 | fullscreen={ 55 | "deadzone": 0.5, 56 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":48,"unicode":0,"echo":false,"script":null) 57 | ] 58 | } 59 | start={ 60 | "deadzone": 0.5, 61 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) 62 | ] 63 | } 64 | 65 | [rendering] 66 | 67 | environment/default_environment="res://default_env.tres" 68 | -------------------------------------------------------------------------------- /Circuitous Circuits.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {2B950B54-DDB8-4BE4-90D8-89C0B443E481} 7 | Library 8 | .mono/temp/bin/$(Configuration) 9 | NewGameProject 10 | Circuitous Circuits 11 | v4.7.2 12 | .mono/temp/obj 13 | $(BaseIntermediateOutputPath)/$(Configuration) 14 | 15 | 16 | true 17 | portable 18 | false 19 | DEBUG; 20 | prompt 21 | 4 22 | false 23 | 24 | 25 | portable 26 | true 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | true 33 | portable 34 | false 35 | DEBUG;TOOLS; 36 | prompt 37 | 4 38 | false 39 | 40 | 41 | 42 | $(ProjectDir)/.mono/assemblies/GodotSharp.dll 43 | False 44 | 45 | 46 | $(ProjectDir)/.mono/assemblies/GodotSharpEditor.dll 47 | False 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Code/Puzzle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Godot; 3 | using Settworks.Hexagons; 4 | 5 | public class Puzzle : Node2D 6 | { 7 | const int levelCount = 10; 8 | 9 | Sprite hexCursor; 10 | Tween snapTween; 11 | 12 | public HexMap Map => map; 13 | public int GetNextPathId() => nextPathId++; 14 | 15 | int nextPathId = 1; 16 | HexCoord spawnCoord; 17 | Node2D board; 18 | HexMap map; 19 | 20 | int currentLevel; 21 | 22 | public Color TileColor { get; private set; } = Colors.Red; 23 | public Color StaticTileColor { get; private set; } = Colors.DarkRed; 24 | public Color BoardColor { get; private set; } = Colors.Blue; 25 | public Color BackgroundColor { get; private set; } = Colors.DarkGray; 26 | public Color LineHighlightColor { get; private set; } = Colors.Yellow; 27 | 28 | public override void _Ready() 29 | { 30 | CreateCursor(); 31 | 32 | snapTween = new Tween(); 33 | AddChild(snapTween); 34 | 35 | InitializeSound(); 36 | LoadMusic(); 37 | StartMusic(); 38 | 39 | LoadLevel(1); 40 | 41 | GetViewport().Connect("size_changed", this, nameof(Rescale)); 42 | } 43 | 44 | public override void _Process(float delta) 45 | { 46 | if (Input.IsActionJustPressed("fullscreen")) 47 | { 48 | OS.WindowFullscreen = !OS.WindowFullscreen; 49 | } 50 | else if (Input.IsActionJustPressed("back")) 51 | { 52 | GetTree().ChangeScene("res://Splash.tscn");//.Quit(); 53 | } 54 | else if (Input.IsActionJustPressed("reset")) 55 | { 56 | ResetLevel(); 57 | } 58 | } 59 | 60 | void CreateCursor () 61 | { 62 | var texture = Resources.Textures.Cursor; 63 | var textureHeight = texture.GetHeight(); 64 | float scale = 2f / textureHeight; 65 | 66 | hexCursor = new Sprite 67 | { 68 | Texture = texture, 69 | Scale = new Vector2(scale, scale), 70 | ZIndex = (int)ZLayers.Cursor, 71 | Visible = false 72 | }; 73 | AddChild(hexCursor); 74 | } 75 | 76 | Sprite AddBoardCell (HexCoord c) 77 | { 78 | var texture = Resources.Textures.Tile; 79 | var textureHeight = texture.GetHeight(); 80 | float scale = 2f / textureHeight; 81 | 82 | var s = new Sprite 83 | { 84 | Texture = texture, 85 | Scale = new Vector2(scale, scale), 86 | ZIndex = (int)ZLayers.Background, 87 | Position = c.Position(), 88 | Modulate = BoardColor 89 | }; 90 | board.AddChild(s); 91 | var overlay = new Sprite 92 | { 93 | Texture = Resources.Textures.BoardGradient, 94 | Scale = new Vector2(scale, scale), 95 | ZIndex = (int)ZLayers.Background+1, 96 | Position = c.Position() 97 | }; 98 | board.AddChild(overlay); 99 | return s; 100 | } 101 | 102 | public void ShowCursor(HexCoord coord) 103 | { 104 | if (!hexCursor.Visible) 105 | { 106 | hexCursor.Position = coord.Position(); 107 | hexCursor.Visible = true; 108 | 109 | } 110 | else 111 | { 112 | snapTween.InterpolateProperty(hexCursor, "position", null, coord.Position(), 0.1f, Tween.TransitionType.Cubic, Tween.EaseType.Out, 0); 113 | snapTween.Start(); 114 | } 115 | } 116 | 117 | public void HideCursor() 118 | { 119 | hexCursor.Visible = false; 120 | } 121 | 122 | public void SnapTileToCell (PuzzleTileHex tile, HexCoord coord) 123 | { 124 | snapTween.InterpolateProperty(tile, "position", null, coord.Position(), 0.1f, Tween.TransitionType.Cubic, Tween.EaseType.Out, 0); 125 | snapTween.Start(); 126 | } 127 | 128 | int tileID = 0; 129 | 130 | public void SpawnTile () 131 | { 132 | var x = PuzzleTileHex.GetRandomTile(this); 133 | x.MakeDraggable(); 134 | x.Name = $"tile_{tileID++}"; 135 | x.Position = spawnCoord.Position(); 136 | board.AddChild(x); 137 | } 138 | 139 | public void ResetTile(PuzzleTileHex tile) => SnapTileToCell(tile, spawnCoord); 140 | 141 | AudioStreamPlayer[] musicLayers; 142 | int activeMusicLayerCount = 0; 143 | 144 | void LoadMusic () 145 | { 146 | AudioStreamOGGVorbis[] musicLayerResources = { 147 | Resources.Music.Melody, 148 | Resources.Music.Bass, 149 | Resources.Music.Swells, 150 | Resources.Music.Decoration, 151 | Resources.Music.Eighths 152 | }; 153 | 154 | musicLayers = new AudioStreamPlayer[musicLayerResources.Length]; 155 | for (int i = 0; i < musicLayers.Length; i++) 156 | { 157 | var player = new AudioStreamPlayer 158 | { 159 | Stream = musicLayerResources[i] 160 | }; 161 | AddChild(player); 162 | musicLayers[i] = player; 163 | } 164 | musicLayers[0].Connect("finished", this, nameof(OnMusicFinished)); 165 | } 166 | 167 | void StartMusic () 168 | { 169 | activeMusicLayerCount = 1; 170 | musicLayers[0].Play(0); 171 | } 172 | 173 | void OnMusicFinished () 174 | { 175 | activeMusicLayerCount++; 176 | for (int i = 0; i < musicLayers.Length; i++) 177 | { 178 | if (i < activeMusicLayerCount) 179 | { 180 | if (musicLayers[i].IsPlaying()) 181 | { 182 | //GODOT: if one of the layers is still playing when we (re)start them all, 183 | //we get a loud click glitch. however, if we wait for all of them to finish 184 | //we sometimes get a slight gap. 185 | GD.Print($"Music layer {i} was out of sync"); 186 | } 187 | musicLayers[i].Play(0); 188 | 189 | } 190 | else 191 | { 192 | musicLayers[i].Stop(); 193 | } 194 | } 195 | } 196 | 197 | void InitializeSound() 198 | { 199 | AudioStreamPlayer CreatePlayer (AudioStreamOGGVorbis s) 200 | { 201 | //GODOT: would be nice if the ctor let us pass the stream in 202 | //GODOT: not obvious what to do for one-shot effects. should i create as needed and destroy when they're done? should I pool players? 203 | var p = new AudioStreamPlayer { Stream = s }; 204 | p.Autoplay = false; 205 | s.Loop = false; 206 | //GODOT: it would be REALLY nice if AddChild returned the child 207 | AddChild(p); 208 | return p; 209 | } 210 | 211 | SoundPlayerRotate = CreatePlayer(Resources.Sfx.RotateTile); 212 | SoundPlayerDrop = CreatePlayer(Resources.Sfx.PlaceTile); 213 | SoundPlayerWhoosh = CreatePlayer(Resources.Sfx.WhooshBack); 214 | SoundPlayerPickup = CreatePlayer(Resources.Sfx.PickUpTile); 215 | SoundPlayerComplete = CreatePlayer(Resources.Sfx.CircuitComplete); 216 | SoundPlayerFail = CreatePlayer(Resources.Sfx.CircuitFail); 217 | } 218 | 219 | 220 | public AudioStreamPlayer SoundPlayerRotate { get; private set; } 221 | public AudioStreamPlayer SoundPlayerDrop { get; private set; } 222 | public AudioStreamPlayer SoundPlayerWhoosh { get; private set; } 223 | public AudioStreamPlayer SoundPlayerPickup { get; private set; } 224 | public AudioStreamPlayer SoundPlayerComplete { get; private set; } 225 | public AudioStreamPlayer SoundPlayerFail { get; private set; } 226 | 227 | void CreateLevel(int size) 228 | { 229 | //reset everything 230 | nextPathId = 1; 231 | map = null; 232 | if (board != null) 233 | { 234 | RemoveChild(board); 235 | board.QueueFree(); 236 | } 237 | board = new Node2D(); 238 | AddChild(board); 239 | HideCursor(); 240 | 241 | map = new HexMap(size); 242 | map.Initialize(c => new CellInfo(c, AddBoardCell(c))); 243 | 244 | spawnCoord = new HexCoord(size * 2, 0); 245 | SpawnTile(); 246 | } 247 | 248 | void LoadLevel (int number) 249 | { 250 | currentLevel = number; 251 | 252 | //GODOT: how on earth do I load a text file? 253 | //var resource = GD.Load($"res://Levels/{number}.txt"); 254 | //this took 30mins to figure out. every operation is different than the 255 | //C# BCL version and the docs are not helpful 256 | var f = new File(); 257 | f.Open($"res://Levels/{number}.txt", (int)File.ModeFlags.Read); 258 | var lines = new System.Collections.Generic.List(); 259 | while(!f.EofReached()) 260 | { 261 | lines.Add(f.GetLine()); 262 | } 263 | //var lines = System.IO.File.ReadAllLines(System.IO.Path.Combine("Levels", $"{number}.txt")); 264 | 265 | var boardDef = lines[0].Split('|'); 266 | int boardSize = int.Parse(boardDef[0]); 267 | if (boardDef.Length == 6) 268 | { 269 | TileColor = new Color(boardDef[1]); 270 | 271 | if (string.IsNullOrEmpty(boardDef[2])) 272 | { 273 | StaticTileColor = TileColor.Darkened(0.2f); 274 | } 275 | else 276 | { 277 | StaticTileColor = new Color(boardDef[2]); 278 | } 279 | 280 | BoardColor = new Color(boardDef[3]); 281 | 282 | if (string.IsNullOrEmpty(boardDef[4])) 283 | { 284 | BackgroundColor = Colors.Black; 285 | } 286 | else 287 | { 288 | BackgroundColor = new Color(boardDef[4]); 289 | } 290 | 291 | LineHighlightColor = new Color(boardDef[5]); 292 | } 293 | else 294 | { 295 | TileColor = new Color("e017c2"); 296 | StaticTileColor = TileColor.Darkened(0.2f); 297 | BoardColor = new Color("bdf0ec"); 298 | BackgroundColor = Colors.Black; 299 | LineHighlightColor = Colors.Yellow; 300 | } 301 | 302 | VisualServer.SetDefaultClearColor(BackgroundColor); 303 | 304 | CreateLevel(boardSize); 305 | 306 | for (int i = 1; i < lines.Count; i++) 307 | { 308 | var line = lines[i]; 309 | if (string.IsNullOrWhiteSpace(line)) 310 | { 311 | continue; 312 | } 313 | 314 | int commaIdx = line.IndexOf(','); 315 | int q = int.Parse(line.Substring(0,commaIdx)); 316 | 317 | var pipeIdx = line.IndexOf('|'); 318 | int r = int.Parse(line.Substring(commaIdx + 1, pipeIdx - commaIdx - 1)); 319 | 320 | 321 | int[] desc = new int[6]; 322 | for (int j = 0; j < 6; j++) 323 | { 324 | desc[j] = line[pipeIdx+ 1+j] - '0'; 325 | } 326 | 327 | //offset due to the map we're using to figure out positions 328 | int offset = Math.Max(0, 4 - boardSize); 329 | var coord = new HexCoord(q, r - offset); 330 | var tile = new PuzzleTileHex { LineDescriptions = desc, Position = coord.Position() }; 331 | tile.IsStatic = true; 332 | map.SetCell(new CellInfo(coord, tile)); 333 | board.AddChild(tile); 334 | tile.CalculatePaths(this, coord); 335 | } 336 | 337 | var c = TileColor; 338 | c.a = 0.6f; 339 | hexCursor.Modulate = c; 340 | 341 | Rescale(); 342 | } 343 | 344 | public void NextLevel() 345 | { 346 | currentLevel = (currentLevel % levelCount) + 1; 347 | LoadLevel(currentLevel); 348 | } 349 | 350 | public void ResetLevel () => LoadLevel(currentLevel); 351 | 352 | public void Rescale () 353 | { 354 | var rect = GetViewportRect(); 355 | 356 | //scale such that the puzzle fits with a half tile margin 357 | 358 | var radius = rect.Size.y /((map.Size * 0.75f + 0.25f) + 1f) / 2f; 359 | Scale = new Vector2(radius, radius); 360 | 361 | var center = new HexCoord(map.EdgeSize - 1, map.EdgeSize - 1); 362 | var centerPos = center.Position() * Scale; 363 | var centerScreen = new Vector2(rect.Size.x / 2f, rect.Size.y / 2f); 364 | 365 | Position = centerScreen - centerPos; 366 | } 367 | } 368 | 369 | enum ZLayers 370 | { 371 | Background = 0, 372 | Cursor = 100, 373 | DroppedTile = 200, 374 | DragTile = 300 375 | } 376 | -------------------------------------------------------------------------------- /Code/PuzzleTileHex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Godot; 3 | using Settworks.Hexagons; 4 | 5 | public class PuzzleTileHex : Node2D 6 | { 7 | bool isUnderMouse; 8 | bool isDragging; 9 | Vector2 mouseOffset; 10 | 11 | Sprite background; 12 | 13 | Area2D dragArea; 14 | Tween snapTween; 15 | 16 | Puzzle puzzle; 17 | 18 | public bool IsStatic { get; set; } 19 | 20 | public override void _Ready() 21 | { 22 | puzzle = (Puzzle)GetParent().GetParent(); 23 | 24 | background = AddSprite(Resources.Textures.Tile); 25 | background.Modulate = IsStatic? puzzle.StaticTileColor : puzzle.TileColor; 26 | 27 | ZIndex = (int)ZLayers.DroppedTile; 28 | 29 | snapTween = new Tween(); 30 | AddChild(snapTween); 31 | 32 | if (LineDescriptions == null) 33 | { 34 | LineDescriptions = new[] { 0, 1, 2, 3, 4, 5 }; 35 | } 36 | 37 | AddLines(); 38 | } 39 | 40 | public int[] LineDescriptions = new int[6]; 41 | public int[] Paths = new int[6]; 42 | readonly Sprite[] pathSprites = new Sprite[6]; 43 | 44 | public Sprite GetPathSprite(int index) => pathSprites[(index - rotations + 6000000) % 6]; 45 | 46 | Sprite AddSprite(Texture texture, int zindex = 0) 47 | { 48 | var textureHeight = texture.GetHeight(); 49 | float scale = 2f / textureHeight; 50 | 51 | var s = new Sprite 52 | { 53 | Texture = texture, 54 | ZAsRelative = true, 55 | ZIndex = zindex, 56 | Scale = new Vector2(scale, scale) 57 | }; 58 | 59 | AddChild(s); 60 | 61 | return s; 62 | } 63 | 64 | void AddLines() 65 | { 66 | for (int i = 0; i < 6; i++) 67 | { 68 | var delta = Constrain(i - LineDescriptions[i]); 69 | if (delta > 3) 70 | { 71 | continue; 72 | } 73 | 74 | int rot = i; 75 | Texture texture; 76 | switch (delta) 77 | { 78 | case 0: 79 | texture = Resources.Textures.Stop; 80 | break; 81 | case 1: 82 | texture = Resources.Textures.CurveSmall; 83 | break; 84 | case 2: 85 | texture = Resources.Textures.CurveLarge; 86 | break; 87 | case 3: 88 | texture = Resources.Textures.Line; 89 | break; 90 | default: 91 | throw new InvalidOperationException(); 92 | } 93 | 94 | var s = AddSprite(texture, 1); 95 | s.Rotation = rot * Mathf.Pi / 3f; 96 | 97 | pathSprites[i] = s; 98 | } 99 | } 100 | 101 | public void MakeDraggable() 102 | { 103 | dragArea = new Area2D(); 104 | AddChild(dragArea); 105 | 106 | dragArea.Connect("mouse_entered", this, nameof(OnMouseEntered)); 107 | dragArea.Connect("mouse_exited", this, nameof(OnMouseExited)); 108 | 109 | var shape = new ConvexPolygonShape2D(); 110 | shape.SetPoints(new Vector2[] 111 | { 112 | new Vector2 (0, -1f), 113 | new Vector2 (1f, -0.5f), 114 | new Vector2 (1f, 0.5f), 115 | new Vector2 (0, 1f), 116 | new Vector2 (-1f, 0.5f), 117 | new Vector2 (-1f, -5f), 118 | }); 119 | 120 | var ownerId = dragArea.CreateShapeOwner(dragArea); 121 | dragArea.ShapeOwnerAddShape(ownerId, shape); 122 | } 123 | 124 | public void MakeFixed() 125 | { 126 | RemoveChild(dragArea); 127 | dragArea.QueueFree(); 128 | dragArea = null; 129 | } 130 | 131 | public bool IsDraggable => dragArea != null; 132 | 133 | // this is true for now 134 | public bool HasFocus => IsDraggable; 135 | 136 | public override void _Process(float delta) 137 | { 138 | if (IsDraggable) 139 | { 140 | HandleDrag(); 141 | } 142 | 143 | if (HasFocus) 144 | { 145 | if (Input.IsActionJustPressed("rotate_right")) 146 | { 147 | RotateRight(); 148 | } 149 | else if (Input.IsActionJustPressed("rotate_left")) 150 | { 151 | RotateLeft(); 152 | } 153 | } 154 | } 155 | 156 | int oldZIndex; 157 | 158 | void HandleDrag() 159 | { 160 | if (!isDragging) 161 | { 162 | if (!isUnderMouse) 163 | { 164 | return; 165 | } 166 | 167 | isDragging = Input.IsActionPressed("left_click"); 168 | if (!isDragging) 169 | { 170 | return; 171 | } 172 | 173 | mouseOffset = Position - GetViewport().GetMousePosition() / GlobalScale; 174 | oldZIndex = ZIndex; 175 | ZIndex = (int)ZLayers.DragTile; 176 | 177 | OnStartDrag(); 178 | } 179 | 180 | if (!Input.IsActionPressed("left_click")) 181 | { 182 | isDragging = false; 183 | OnDrop(); 184 | return; 185 | } 186 | 187 | Position = GetViewport().GetMousePosition() / GlobalScale + mouseOffset; 188 | var coord = HexCoord.AtPosition(Position); 189 | 190 | var mapCell = puzzle.Map.TryGetCell(coord); 191 | if (!IsValidDrop(mapCell)) 192 | { 193 | puzzle.HideCursor(); 194 | return; 195 | } 196 | 197 | puzzle.ShowCursor(coord); 198 | } 199 | 200 | bool IsValidDrop(CellInfo? mapCell) => mapCell != null && !(mapCell.Value.Tile is PuzzleTileHex); 201 | 202 | void OnStartDrag() 203 | { 204 | puzzle.SoundPlayerPickup.Play(0); 205 | } 206 | 207 | void OnDrop() 208 | { 209 | ZIndex = oldZIndex; 210 | puzzle.HideCursor(); 211 | 212 | var coord = HexCoord.AtPosition(Position); 213 | var mapCell = puzzle.Map.TryGetCell(coord); 214 | 215 | if (!IsValidDrop(mapCell)) 216 | { 217 | puzzle.SoundPlayerWhoosh.Play(0); 218 | puzzle.ResetTile(this); 219 | return; 220 | } 221 | 222 | puzzle.SoundPlayerDrop.Play(0); 223 | 224 | MakeFixed(); 225 | 226 | puzzle.Map.SetCell(new CellInfo(coord, this)); 227 | puzzle.SnapTileToCell(this, coord); 228 | puzzle.SpawnTile(); 229 | 230 | CalculatePaths(puzzle, coord); 231 | 232 | if (puzzle.Map.TileCount == puzzle.Map.CellCount) 233 | { 234 | OnFail(); 235 | } 236 | } 237 | 238 | public void CalculatePaths(Puzzle puzzle, HexCoord coord) 239 | { 240 | GD.Print($"Placing tile {Name} ({GetPathString()})"); 241 | 242 | //propagate neighbor paths 243 | for (int side = 0; side < directions.Length; side++) 244 | { 245 | var neighborCell = puzzle.Map.TryGetCell(coord + directions[side]); 246 | if (neighborCell != null) 247 | { 248 | if (neighborCell.Value.Tile is PuzzleTileHex neighbor) 249 | { 250 | GD.Print($"Side {side} neighbor is {neighbor.Name} ({neighbor.GetPathString()})"); 251 | //get the path id of the neighboring tile 252 | var pathID = neighbor.Paths[(side + 3) % 6]; 253 | GD.Print($"Neighbor path ID is {pathID}"); 254 | PropagateIncomingPath(side, pathID); 255 | } 256 | } 257 | } 258 | 259 | //ensure all sides have path IDs 260 | for (int side = 0; side < directions.Length; side++) 261 | { 262 | if (Paths[side] == 0) 263 | { 264 | var id = puzzle.GetNextPathId(); 265 | Paths[side] = id; 266 | Paths[LineDescriptions[side]] = id; 267 | } 268 | } 269 | } 270 | 271 | void PropagateIncomingPath (int side, int pathID) 272 | { 273 | //if this tile side does not have a path ID, propagate it 274 | if (Paths[side] == 0) 275 | { 276 | Paths[side] = pathID; 277 | PathIncrement(pathID); 278 | } 279 | //if it does, join the paths 280 | else 281 | { 282 | pathID = PathJoin(Paths[side], pathID); 283 | } 284 | 285 | //check the internal connection in this tile 286 | var connectedSide = LineDescriptions[side]; 287 | 288 | //if it's a dead end, kill the path 289 | if (connectedSide == side) 290 | { 291 | PathDead(pathID); 292 | return; 293 | } 294 | 295 | GD.Print($"Side {side} is internally connected to side {connectedSide}"); 296 | 297 | var internalConnectedPath = Paths[connectedSide]; 298 | 299 | // if the internal connection has no path ID, propagate this one, 300 | // else join them 301 | if (internalConnectedPath == 0) 302 | { 303 | Paths[connectedSide] = pathID; 304 | PathIncrement(pathID); 305 | } 306 | //if it wasn't handled by previous join, join it 307 | else if (internalConnectedPath != pathID) 308 | { 309 | PathJoin(internalConnectedPath, pathID); 310 | } 311 | 312 | } 313 | 314 | void PathDead(int pathID) 315 | { 316 | GD.Print($"Path {pathID} is dead"); 317 | } 318 | 319 | int PathJoin(int oldPath, int newPath) 320 | { 321 | if (oldPath == newPath) 322 | { 323 | GD.Print($"PATH {newPath} IS A CIRCUIT"); 324 | OnSuccess(newPath); 325 | return newPath; 326 | } 327 | 328 | foreach (var tile in puzzle.Map.GetAllTiles()) 329 | { 330 | for (int i = 0; i < 6; i++) 331 | { 332 | if (tile.Paths[i] == oldPath) 333 | { 334 | tile.Paths[i] = newPath; 335 | } 336 | } 337 | } 338 | GD.Print($"Path {oldPath} was consumed by path {newPath}"); 339 | return newPath; 340 | } 341 | 342 | bool isEnded; 343 | 344 | void OnSuccess(int path) 345 | { 346 | if (isEnded) 347 | { 348 | return; 349 | } 350 | isEnded = true; 351 | 352 | var timer = AddChild2(new Timer { OneShot = true }); 353 | timer.Connect("timeout", puzzle, nameof(puzzle.NextLevel)); 354 | timer.Start(2f); 355 | 356 | puzzle.SoundPlayerComplete.Play(0); 357 | TintPath(path, puzzle.LineHighlightColor, 1f); 358 | } 359 | 360 | void OnFail() 361 | { 362 | if (isEnded) 363 | { 364 | return; 365 | } 366 | isEnded = true; 367 | 368 | var timer = AddChild2(new Timer { OneShot = true }); 369 | timer.Connect("timeout", puzzle, nameof(puzzle.ResetLevel)); 370 | timer.Start(1f); 371 | 372 | puzzle.SoundPlayerFail.Play(0); 373 | 374 | foreach (var tile in puzzle.Map.GetAllTiles()) 375 | { 376 | snapTween.InterpolateProperty( 377 | tile.background, "modulate", null, puzzle.BackgroundColor, 1f, 378 | Tween.TransitionType.Sine, Tween.EaseType.In); 379 | snapTween.Start(); 380 | 381 | for (int i = 0; i < 6; i++) 382 | { 383 | var s = tile.pathSprites[i]; 384 | if (s!= null) 385 | { 386 | snapTween.InterpolateProperty( 387 | s, "modulate", null, puzzle.BackgroundColor, 1f, 388 | Tween.TransitionType.Sine, Tween.EaseType.In); 389 | snapTween.Start(); 390 | } 391 | } 392 | } 393 | } 394 | 395 | void PathIncrement (int pathID) 396 | { 397 | GD.Print($"Incrementing path {pathID}"); 398 | } 399 | 400 | void TintPath(int pathID, Color color, float pulseTime = 0f) 401 | { 402 | foreach (var tile in puzzle.Map.GetAllTiles()) 403 | { 404 | for (int i = 0; i < 6; i++) 405 | { 406 | if (tile.Paths[i] == pathID) 407 | { 408 | var sprite = tile.GetPathSprite(i); 409 | if (sprite != null) 410 | { 411 | sprite.ZIndex = 2; 412 | if (pulseTime == 0f) 413 | { 414 | sprite.Modulate = color; 415 | } 416 | else 417 | { 418 | var initial = sprite.Modulate; 419 | snapTween.InterpolateProperty( 420 | sprite, "modulate", null, color, pulseTime*.45f, 421 | Tween.TransitionType.Cubic, Tween.EaseType.Out); 422 | snapTween.InterpolateProperty( 423 | sprite, "modulate", color, initial, pulseTime * .45f, 424 | Tween.TransitionType.Cubic, Tween.EaseType.In, pulseTime * .55f); 425 | snapTween.Start(); 426 | } 427 | } 428 | } 429 | } 430 | } 431 | } 432 | 433 | static HexCoord[] directions = { 434 | new HexCoord (-1, 1), 435 | new HexCoord (-1, 0), 436 | new HexCoord ( 0, -1), 437 | new HexCoord ( 1, -1), 438 | new HexCoord ( 1, 0), 439 | new HexCoord ( 0, 1) 440 | }; 441 | 442 | public void OnMouseEntered() 443 | { 444 | isUnderMouse = true; 445 | } 446 | 447 | public void OnMouseExited() 448 | { 449 | isUnderMouse = false; 450 | } 451 | 452 | public void RotateRight() 453 | { 454 | puzzle.SoundPlayerRotate.Play(0); 455 | 456 | rotations++; 457 | RotateDefinitionRight(LineDescriptions); 458 | AnimateRotation(); 459 | } 460 | 461 | public void RotateLeft() 462 | { 463 | puzzle.SoundPlayerRotate.Play(0); 464 | 465 | rotations--; 466 | int tmp = LineDescriptions[0]; 467 | for (int i = 0; i < LineDescriptions.Length - 1; i++) 468 | { 469 | LineDescriptions[i] = Constrain(LineDescriptions[i + 1] - 1); 470 | } 471 | LineDescriptions[5] = Constrain(tmp - 1); 472 | AnimateRotation(); 473 | } 474 | 475 | int rotations; 476 | 477 | void AnimateRotation() 478 | { 479 | var newRotation = rotations * Mathf.Pi / 3f; 480 | snapTween.InterpolateProperty(this, "rotation", null, newRotation, 0.2f, Tween.TransitionType.Cubic, Tween.EaseType.Out, 0); 481 | snapTween.Start(); 482 | } 483 | 484 | static int Constrain(int i) => (i + 6) % 6; 485 | 486 | static readonly int[][] tilesDefinitions = { 487 | new[] { 0, 1, 2, 3, 4, 5 }, // all ends 488 | new[] { 1, 0, 2, 3, 4, 5 }, // short and 4 ends 489 | new[] { 1, 0, 3, 2, 4, 5 }, // 2 short, adjacent 490 | new[] { 1, 0, 2, 4, 3, 5 }, // 2 short, ends between them 491 | new[] { 1, 0, 3, 2, 5, 4 }, // 3 short 492 | 493 | new[] { 2, 1, 0, 3, 4, 5 }, // 1 long 494 | new[] { 2, 1, 0, 5, 4, 3 }, // 2 long opposite, 2 ends 495 | new[] { 2, 3, 0, 1, 4, 5 }, // 2 long adjacent, 2 ends 496 | new[] { 2, 3, 0, 1, 5, 4 }, // 2 long adjacent, 1 short 497 | new[] { 2, 4, 0, 5, 1, 3 }, // 2 long opposite, 1 straight 498 | 499 | new[] { 2, 1, 0, 4, 3, 5 }, // long, short, end 500 | new[] { 2, 1, 0, 3, 5, 4 }, // long, end, short 501 | new[] { 2, 4, 0, 3, 1, 5 }, // long, end, straight, end 502 | new[] { 1, 0, 5, 4, 3, 2 }, // short, straight, short 503 | 504 | new[] { 3, 4, 5, 0, 1, 2 }, // 3 straight 505 | new[] { 3, 4, 2, 0, 1, 5 }, // 2 straight 506 | new[] { 3, 1, 2, 0, 4, 5 }, // 1 straight 507 | }; 508 | 509 | static Random random = new Random(); 510 | 511 | public static PuzzleTileHex GetRandomTile(Puzzle puzzle) 512 | { 513 | var idx = random.Next(0, tilesDefinitions.Length - 1); 514 | var def = (int[])tilesDefinitions[idx].Clone(); 515 | var rotations = random.Next(0, 5); 516 | for (int i = 0; i < rotations; i++) 517 | { 518 | RotateDefinitionRight(def); 519 | } 520 | return new PuzzleTileHex { LineDescriptions = def }; 521 | } 522 | 523 | static void RotateDefinitionRight(int[] definition) 524 | { 525 | int tmp = definition[definition.Length - 1]; 526 | for (int i = definition.Length - 1; i > 0; i--) 527 | { 528 | definition[i] = Constrain(definition[i - 1] + 1); 529 | } 530 | definition[0] = Constrain(tmp + 1); 531 | } 532 | 533 | string GetPathString () => $"{LineDescriptions[0]}{LineDescriptions[1]}{LineDescriptions[2]}{LineDescriptions[3]}{LineDescriptions[4]}{LineDescriptions[5]}"; 534 | 535 | //GODOT: this cleans up code a lot 536 | T AddChild2(T child) where T : Node 537 | { 538 | AddChild(child); 539 | return child; 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /Code/HexCoord.cs: -------------------------------------------------------------------------------- 1 | // The MIT License(MIT) 2 | // 3 | // Copyright(c) 2014 Theodore Lief Gannon 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | // this software and associated documentation files (the "Software"), to deal in 7 | // the Software without restriction, including without limitation the rights to 8 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | // the Software, and to permit persons to whom the Software is furnished to do so, 10 | // subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | using Godot; 23 | using System; 24 | using System.Collections.Generic; 25 | 26 | namespace Settworks.Hexagons 27 | { 28 | /// 29 | /// Hexagon grid coordinate. 30 | /// 31 | /// 32 | /// Uses the q,r axial system detailed at http://www.redblobgames.com/grids/hexagons/. 33 | /// These are "pointy topped" hexagons. The q axis points right, and the r axis points up-right. 34 | /// When converting to and from Unity coordinates, the length of a hexagon side is 1 unit. 35 | /// 36 | [Serializable] 37 | public struct HexCoord 38 | { 39 | 40 | /// 41 | /// Position on the q axis. 42 | /// ] 43 | public int q; 44 | /// 45 | /// Position on the r axis. 46 | /// 47 | public int r; 48 | 49 | /// 50 | /// Initializes a new instance of the struct. 51 | /// 52 | /// Position on the q axis. 53 | /// Position on the r axis. 54 | public HexCoord(int q, int r) 55 | { 56 | this.q = q; 57 | this.r = r; 58 | } 59 | 60 | /// 61 | /// Position on the cubic z axis. 62 | /// 63 | /// 64 | /// The q,r coordinate system is derived from an x,y,z cubic system with the constraint that x + y + z = 0. 65 | /// Where x = q and y = r, this property derives z as -q-r. 66 | /// 67 | public int Z 68 | { 69 | get { return -q - r; } 70 | } 71 | 72 | /// 73 | /// Offset x coordinate. 74 | /// 75 | /// 76 | /// Offset coordinates are a common alternative for hexagons, allowing pseudo-square grid operations. 77 | /// Where y = r, this property represents the x coordinate as q + r/2. 78 | /// 79 | public int O 80 | { 81 | get { return q + (r >> 1); } 82 | } 83 | 84 | /// 85 | /// Unity position of this hex. 86 | /// 87 | public Vector2 Position() 88 | { 89 | return q * Q_XY + r * R_XY; 90 | } 91 | 92 | /// 93 | /// Get the maximum absolute cubic coordinate. 94 | /// 95 | /// 96 | /// In hexagonal space this is the polar radius, i.e. distance from 0,0. 97 | /// 98 | public int AxialLength() 99 | { 100 | if (q == 0 && r == 0) return 0; 101 | if (q > 0 && r >= 0) return q + r; 102 | if (q <= 0 && r > 0) return (-q < r) ? r : -q; 103 | if (q < 0) return -q - r; 104 | return (-r > q) ? -r : q; 105 | } 106 | 107 | /// 108 | /// Get the minimum absolute cubic coordinate. 109 | /// 110 | /// 111 | /// This is the number of hexagon steps from 0,0 which are not along the maximum axis. 112 | /// 113 | public int AxialSkew() 114 | { 115 | if (q == 0 && r == 0) return 0; 116 | if (q > 0 && r >= 0) return (q < r) ? q : r; 117 | if (q <= 0 && r > 0) return (-q < r) ? Math.Min(-q, q + r) : Math.Min(r, -q - r); 118 | if (q < 0) return (q > r) ? -q : -r; 119 | return (-r > q) ? Math.Min(q, -q - r) : Math.Min(-r, q + r); 120 | } 121 | 122 | /// 123 | /// Get the angle from 0,0 to the center of this hex. 124 | /// 125 | public float PolarAngle() 126 | { 127 | Vector2 pos = Position(); 128 | return Mathf.Atan2(pos.y, pos.x); 129 | } 130 | 131 | /// 132 | /// Get the counterclockwise position of this hex in the ring at its distance from 0,0. 133 | /// 134 | public int PolarIndex() 135 | { 136 | if (q == 0 && r == 0) return 0; 137 | if (q > 0 && r >= 0) return r; 138 | if (q <= 0 && r > 0) return (-q < r) ? r - q : -3 * q - r; 139 | if (q < 0) return -4 * (q + r) + q; 140 | return (-r > q) ? -4 * r + q : 6 * q + r; 141 | } 142 | 143 | /// 144 | /// Get a neighboring hex. 145 | /// 146 | /// 147 | /// Neighbor 0 is to the right, others proceed counterclockwise. 148 | /// 149 | /// Index of the desired neighbor. Cyclically constrained 0..5. 150 | public HexCoord Neighbor(int index) 151 | { 152 | return NeighborVector(index) + this; 153 | } 154 | 155 | public HexCoord PolarNeighbor(bool CCW = false) 156 | { 157 | if (q > 0) 158 | { 159 | if (r < 0) 160 | { 161 | if (q > -r) return this + neighbors[CCW ? 1 : 4]; 162 | if (q < -r) return this + neighbors[CCW ? 0 : 3]; 163 | return this + neighbors[CCW ? 1 : 3]; 164 | } 165 | if (r > 0) return this + neighbors[CCW ? 2 : 5]; 166 | return this + neighbors[CCW ? 2 : 4]; 167 | } 168 | if (q < 0) 169 | { 170 | if (r > 0) 171 | { 172 | if (r > -q) return this + neighbors[CCW ? 3 : 0]; 173 | if (r < -q) return this + neighbors[CCW ? 4 : 1]; 174 | return this + neighbors[CCW ? 4 : 0]; 175 | } 176 | if (r < 0) return this + neighbors[CCW ? 5 : 2]; 177 | return this + neighbors[CCW ? 5 : 1]; 178 | } 179 | if (r > 0) return this + neighbors[CCW ? 3 : 5]; 180 | if (r < 0) return this + neighbors[CCW ? 0 : 2]; 181 | return this; 182 | } 183 | 184 | /// 185 | /// Enumerate this hex's six neighbors. 186 | /// 187 | /// 188 | /// Neighbor 0 is to the right, others proceed counterclockwise. 189 | /// 190 | /// Index of the first neighbor to enumerate. 191 | public IEnumerable Neighbors(int first = 0) 192 | { 193 | foreach (HexCoord hex in NeighborVectors(first)) 194 | yield return hex + this; 195 | } 196 | 197 | /// 198 | /// Get the Unity position of a corner vertex. 199 | /// 200 | /// 201 | /// Corner 0 is at the upper right, others proceed counterclockwise. 202 | /// 203 | /// Index of the desired corner. Cyclically constrained 0..5. 204 | public Vector2 Corner(int index) 205 | { 206 | return CornerVector(index) + Position(); 207 | } 208 | 209 | /// 210 | /// Enumerate this hex's six corners. 211 | /// 212 | /// 213 | /// Corner 0 is at the upper right, others proceed counterclockwise. 214 | /// 215 | /// Index of the first corner to enumerate. 216 | public IEnumerable Corners(int first = 0) 217 | { 218 | Vector2 pos = Position(); 219 | foreach (Vector2 v in CornerVectors(first)) 220 | yield return v + pos; 221 | } 222 | 223 | /// 224 | /// Get the polar angle to a corner vertex. 225 | /// 226 | /// 227 | /// This is the angle in radians from the center of 0,0 to the selected corner of this hex. 228 | /// 229 | /// Index of the desired corner. 230 | public float CornerPolarAngle(int index) 231 | { 232 | Vector2 pos = Corner(index); 233 | return Mathf.Atan2(pos.y, pos.x); 234 | } 235 | 236 | /// 237 | /// Get the polar angle to the clockwise bounding corner. 238 | /// 239 | /// 240 | /// The two polar bounding corners are those whose polar angles form the widest arc. 241 | /// 242 | /// If set to true, gets the counterclockwise bounding corner. 243 | public float PolarBoundingAngle(bool CCW = false) 244 | { 245 | return CornerPolarAngle(PolarBoundingCornerIndex(CCW)); 246 | } 247 | 248 | /// 249 | /// Get the XY position of the clockwise bounding corner. 250 | /// 251 | /// 252 | /// The two polar bounding corners are those whose polar angles form the widest arc. 253 | /// 254 | /// If set to true, gets the counterclockwise bounding corner. 255 | public Vector2 PolarBoundingCorner(bool CCW = false) 256 | { 257 | return Corner(PolarBoundingCornerIndex(CCW)); 258 | } 259 | 260 | /// 261 | /// Get the index of the clockwise bounding corner. 262 | /// 263 | /// 264 | /// The two polar bounding corners are those whose polar angles form the widest arc. 265 | /// 266 | /// If set to true, gets the counterclockwise bounding corner. 267 | public int PolarBoundingCornerIndex(bool CCW = false) 268 | { 269 | if (q == 0 && r == 0) return 0; 270 | if (q > 0 && r >= 0) return CCW ? 271 | (q > r) ? 1 : 2 : 272 | (q < r) ? 5 : 4; 273 | if (q <= 0 && r > 0) return (-q < r) ? 274 | CCW ? 275 | (r > -2 * q) ? 2 : 3 : 276 | (r < -2 * q) ? 0 : 5 : 277 | CCW ? 278 | (q > -2 * r) ? 3 : 4 : 279 | (q < -2 * r) ? 1 : 0; 280 | if (q < 0) return CCW ? 281 | (q < r) ? 4 : 5 : 282 | (q > r) ? 2 : 1; 283 | return (-r > q) ? 284 | CCW ? 285 | (r < -2 * q) ? 5 : 0 : 286 | (r > -2 * q) ? 3 : 2 : 287 | CCW ? 288 | (q < -2 * r) ? 0 : 1 : 289 | (q > -2 * r) ? 4 : 3; 290 | } 291 | 292 | /// 293 | /// Get the half sextant of origin containing this hex. 294 | /// 295 | /// 296 | /// CornerSextant is HalfSextant/2. NeighborSextant is (HalfSextant+1)/2. 297 | /// 298 | public int HalfSextant() 299 | { 300 | if (q > 0 && r >= 0 || q == 0 && r == 0) 301 | return (q > r) ? 0 : 1; 302 | if (q <= 0 && r > 0) 303 | return (-q < r) ? 304 | (r > -2 * q) ? 2 : 3 : 305 | (q > -2 * r) ? 4 : 5; 306 | if (q < 0) 307 | return (q < r) ? 6 : 7; 308 | return (-r > q) ? 309 | (r < -2 * q) ? 8 : 9 : 310 | (q < -2 * r) ? 10 : 11; 311 | } 312 | 313 | /// 314 | /// Get the corner index of 0,0 closest to this hex's polar vector. 315 | /// 316 | public int CornerSextant() 317 | { 318 | if (q > 0 && r >= 0 || q == 0 && r == 0) return 0; 319 | if (q <= 0 && r > 0) return (-q < r) ? 1 : 2; 320 | if (q < 0) return 3; 321 | return (-r > q) ? 4 : 5; 322 | } 323 | 324 | /// 325 | /// Get the neighbor index of 0,0 through which this hex's polar vector passes. 326 | /// 327 | public int NeighborSextant() 328 | { 329 | if (q == 0 && r == 0) return 0; 330 | if (q > 0 && r >= 0) return (q <= r) ? 1 : 0; 331 | if (q <= 0 && r > 0) return (-q <= r) ? 332 | (r <= -2 * q) ? 2 : 1 : 333 | (q <= -2 * r) ? 3 : 2; 334 | if (q < 0) return (q >= r) ? 4 : 3; 335 | return (-r > q) ? 336 | (r >= -2 * q) ? 5 : 4 : 337 | (q >= -2 * r) ? 0 : 5; 338 | } 339 | 340 | /// 341 | /// Rotate around 0,0 in sextant increments. 342 | /// 343 | /// 344 | /// A new representing this one after rotation. 345 | /// 346 | /// How many sextants to rotate by. 347 | public HexCoord SextantRotation(int sextants) 348 | { 349 | if (this == origin) return this; 350 | sextants = NormalizeRotationIndex(sextants, 6); 351 | if (sextants == 0) return this; 352 | if (sextants == 1) return new HexCoord(-r, -Z); 353 | if (sextants == 2) return new HexCoord(Z, q); 354 | if (sextants == 3) return new HexCoord(-q, -r); 355 | if (sextants == 4) return new HexCoord(r, Z); 356 | return new HexCoord(-Z, -q); 357 | } 358 | 359 | /// 360 | /// Mirror across a cubic axis. 361 | /// 362 | /// 363 | /// The cubic axes are "diagonal" to the hexagons, passing through two opposite corners. 364 | /// 365 | /// A corner index through which the axis passes. 366 | /// A new representing this one after mirroring. 367 | public HexCoord Mirror(int axis = 1) 368 | { 369 | if (this == origin) return this; 370 | axis = NormalizeRotationIndex(axis, 3); 371 | if (axis == 0) return new HexCoord(r, q); 372 | if (axis == 1) return new HexCoord(Z, r); 373 | return new HexCoord(q, Z); 374 | } 375 | 376 | /// 377 | /// Scale as a vector, truncating result. 378 | /// 379 | /// This after scaling. 380 | public HexCoord Scale(float factor) 381 | { 382 | q = (int)(q * factor); 383 | r = (int)(r * factor); 384 | return this; 385 | } 386 | /// 387 | /// Scale as a vector. 388 | /// 389 | /// This after scaling. 390 | public HexCoord Scale(int factor) 391 | { 392 | q *= factor; 393 | r *= factor; 394 | return this; 395 | } 396 | /// 397 | /// Scale as a vector. 398 | /// 399 | /// representing the scaled vector. 400 | public Vector2 ScaleToVector(float factor) 401 | { return new Vector2(q * factor, r * factor); } 402 | 403 | /// 404 | /// Determines whether this hex is within a specified rectangle. 405 | /// 406 | /// true if this instance is within the specified rectangle; otherwise, false. 407 | public bool IsWithinRectangle(HexCoord cornerA, HexCoord cornerB) 408 | { 409 | if (r > cornerA.r && r > cornerB.r || r < cornerA.r && r < cornerB.r) 410 | return false; 411 | bool reverse = cornerA.O > cornerB.O; // Travel right to left. 412 | bool offset = cornerA.r % 2 != 0; // Starts on an odd row, bump alternate rows left. 413 | bool trim = Math.Abs(cornerA.r - cornerB.r) % 2 == 0; // Even height, trim alternate rows. 414 | bool odd = (r - cornerA.r) % 2 != 0; // This is an alternate row. 415 | int width = Math.Abs(cornerA.O - cornerB.O); 416 | bool hasWidth = width != 0; 417 | if (reverse && (odd && (trim || !offset) || !(trim || offset || odd)) 418 | || !reverse && (trim && odd || offset && !trim && hasWidth)) 419 | width -= 1; 420 | int x = (O - cornerA.O) * (reverse ? -1 : 1); 421 | if (reverse && odd && !offset 422 | || !reverse && offset && odd && hasWidth) 423 | x -= 1; 424 | return (x <= width && x >= 0); 425 | } 426 | 427 | /// 428 | /// Determines whether this hex is on the infinite line passing through points a and b. 429 | /// 430 | public bool IsOnCartesianLine(Vector2 a, Vector2 b) 431 | { 432 | Vector2 AB = b - a; 433 | bool bias = AB.Cross3 (Corner(0) - a).z > 0; 434 | for (int i = 1; i < 6; i++) 435 | { 436 | if (bias != (AB.Cross3 (Corner(i) - a).z > 0)) 437 | return true; 438 | } 439 | return false; 440 | } 441 | 442 | /// 443 | /// Determines whether this the is on the line segment between points a and b. 444 | /// 445 | public bool IsOnCartesianLineSegment(Vector2 a, Vector2 b) 446 | { 447 | Vector2 AB = b - a; 448 | float mag = AB.LengthSquared(); 449 | Vector2 AC = Corner(0) - a; 450 | bool within = AC.LengthSquared() <= mag && AB.Dot(AC) >= 0; 451 | int sign = Math.Sign(AB.Cross3 (AC).z); 452 | for (int i = 1; i < 6; i++) 453 | { 454 | AC = Corner(i) - a; 455 | bool newWithin = AC.LengthSquared() <= mag && AB.Dot(AC) >= 0; 456 | int newSign = Math.Sign(AB.Cross3(AC).z); 457 | if ((within || newWithin) && (sign * newSign <= 0)) 458 | return true; 459 | within = newWithin; 460 | sign = newSign; 461 | } 462 | return false; 463 | } 464 | 465 | /// 466 | /// Returns a that represents the current . 467 | /// 468 | /// 469 | /// Matches the formatting of . 470 | /// 471 | public override string ToString() 472 | { 473 | return "(" + q + ", " + r + ")"; 474 | } 475 | 476 | /* 477 | * Static Methods 478 | */ 479 | 480 | /// 481 | /// HexCoord at (0,0) 482 | /// 483 | public static readonly HexCoord origin; 484 | 485 | /// 486 | /// Distance between two hexes. 487 | /// 488 | public static int Distance(HexCoord a, HexCoord b) 489 | { 490 | return (a - b).AxialLength(); 491 | } 492 | 493 | /// 494 | /// Normalize a rotation index within 0 <= index < cycle. 495 | /// 496 | public static int NormalizeRotationIndex(int index, int cycle = 6) 497 | { 498 | if (index < 0 ^ cycle < 0) 499 | return (index % cycle + cycle) % cycle; 500 | else 501 | return index % cycle; 502 | } 503 | 504 | /// 505 | /// Determine the equality of two rotation indices for a given cycle. 506 | /// 507 | public static bool IsSameRotationIndex(int a, int b, int cycle = 6) 508 | { 509 | return 0 == NormalizeRotationIndex(a - b, cycle); 510 | } 511 | 512 | /// 513 | /// Vector from a hex to a neighbor. 514 | /// 515 | /// 516 | /// Neighbor 0 is to the right, others proceed counterclockwise. 517 | /// 518 | /// Index of the desired neighbor vector. Cyclically constrained 0..5. 519 | public static HexCoord NeighborVector(int index) 520 | { return neighbors[NormalizeRotationIndex(index, 6)]; } 521 | 522 | /// 523 | /// Enumerate the six neighbor vectors. 524 | /// 525 | /// 526 | /// Neighbor 0 is to the right, others proceed counterclockwise. 527 | /// 528 | /// Index of the first neighbor vector to enumerate. 529 | public static IEnumerable NeighborVectors(int first = 0) 530 | { 531 | first = NormalizeRotationIndex(first, 6); 532 | for (int i = first; i < 6; i++) 533 | yield return neighbors[i]; 534 | for (int i = 0; i < first; i++) 535 | yield return neighbors[i]; 536 | } 537 | 538 | /// 539 | /// Neighbor index of 0,0 through which a polar angle passes. 540 | /// 541 | public static int AngleToNeighborIndex(float angle) 542 | { return Mathf.RoundToInt(angle / SEXTANT); } 543 | 544 | /// 545 | /// Polar angle for a neighbor of 0,0. 546 | /// 547 | public static float NeighborIndexToAngle(int index) 548 | { return index * SEXTANT; } 549 | 550 | /// 551 | /// Unity position vector from hex center to a corner. 552 | /// 553 | /// 554 | /// Corner 0 is at the upper right, others proceed counterclockwise. 555 | /// 556 | /// Index of the desired corner. Cyclically constrained 0..5. 557 | public static Vector2 CornerVector(int index) 558 | { 559 | return corners[NormalizeRotationIndex(index, 6)]; 560 | } 561 | 562 | /// 563 | /// Enumerate the six corner vectors. 564 | /// 565 | /// 566 | /// Corner 0 is at the upper right, others proceed counterclockwise. 567 | /// 568 | /// Index of the first corner vector to enumerate. 569 | public static IEnumerable CornerVectors(int first = 0) 570 | { 571 | if (first == 0) 572 | { 573 | foreach (Vector2 v in corners) 574 | yield return v; 575 | } 576 | else 577 | { 578 | first = NormalizeRotationIndex(first, 6); 579 | for (int i = first; i < 6; i++) 580 | yield return corners[i]; 581 | for (int i = 0; i < first; i++) 582 | yield return corners[i]; 583 | } 584 | } 585 | 586 | /// 587 | /// Corner of 0,0 closest to a polar angle. 588 | /// 589 | public static int AngleToCornerIndex(float angle) 590 | { return Mathf.FloorToInt(angle / SEXTANT); } 591 | 592 | /// 593 | /// Polar angle for a corner of 0,0. 594 | /// 595 | public static float CornerIndexToAngle(int index) 596 | { return (index + 0.5f) * SEXTANT; } 597 | 598 | /// 599 | /// Half sextant of 0,0 through which a polar angle passes. 600 | /// 601 | public static int AngleToHalfSextant(float angle) 602 | { return Mathf.RoundToInt(2 * angle / SEXTANT); } 603 | 604 | /// 605 | /// Polar angle at which a half sextant begins. 606 | /// 607 | public static float HalfSextantToAngle(int index) 608 | { return index * SEXTANT / 2; } 609 | 610 | 611 | /// 612 | /// containing a Unity position. 613 | /// 614 | public static HexCoord AtPosition(Vector2 position) 615 | { return FromQRVector(VectorXYtoQR(position)); } 616 | 617 | /// 618 | /// from hexagonal polar coordinates. 619 | /// 620 | /// 621 | /// Hexagonal polar coordinates approximate a circle to a hexagonal ring. 622 | /// 623 | /// Hex distance from 0,0. 624 | /// Counterclockwise index. 625 | public static HexCoord AtPolar(int radius, int index) 626 | { 627 | if (radius == 0) return origin; 628 | if (radius < 0) radius = -radius; 629 | index = NormalizeRotationIndex(index, radius * 6); 630 | int sextant = index / radius; 631 | index %= radius; 632 | if (sextant == 0) return new HexCoord(radius - index, index); 633 | if (sextant == 1) return new HexCoord(-index, radius); 634 | if (sextant == 2) return new HexCoord(-radius, radius - index); 635 | if (sextant == 3) return new HexCoord(index - radius, -index); 636 | if (sextant == 4) return new HexCoord(index, -radius); 637 | return new HexCoord(radius, index - radius); 638 | } 639 | 640 | /// 641 | /// Find the hexagonal polar index closest to angle at radius. 642 | /// 643 | /// 644 | /// Hexagonal polar coordinates approximate a circle to a hexagonal ring. 645 | /// 646 | /// Hex distance from 0,0. 647 | /// Desired polar angle. 648 | public static int FindPolarIndex(int radius, float angle) 649 | { 650 | return (int)Math.Round(angle * radius * 3 / (float)Math.PI); 651 | } 652 | 653 | /// 654 | /// from offset coordinates. 655 | /// 656 | /// 657 | /// Offset coordinates are a common alternative for hexagons, allowing pseudo-square grid operations. 658 | /// This conversion assumes an offset of x = q + r/2. 659 | /// 660 | public static HexCoord AtOffset(int x, int y) 661 | { 662 | return new HexCoord(x - (y >> 1), y); 663 | } 664 | 665 | /// 666 | /// containing a floating-point q,r vector. 667 | /// 668 | /// 669 | /// Hexagonal geometry makes normal rounding inaccurate. If working with floating-point 670 | /// q,r vectors, use this method to accurately convert them back to 671 | /// . 672 | /// 673 | public static HexCoord FromQRVector(Vector2 QRvector) 674 | { 675 | float z = -QRvector.x - QRvector.y; 676 | int ix = (int)Math.Round(QRvector.x); 677 | int iy = (int)Math.Round(QRvector.y); 678 | int iz = (int)Math.Round(z); 679 | if (ix + iy + iz != 0) 680 | { 681 | float dx = Math.Abs(ix - QRvector.x); 682 | float dy = Math.Abs(iy - QRvector.y); 683 | float dz = Math.Abs(iz - z); 684 | if (dx >= dy && dx >= dz) 685 | ix = -iy - iz; 686 | else if (dy >= dz) 687 | iy = -ix - iz; 688 | } 689 | return new HexCoord(ix, iy); 690 | } 691 | 692 | /// 693 | /// Convert an x,y vector to a q,r vector. 694 | /// 695 | public static Vector2 VectorXYtoQR(Vector2 XYvector) 696 | { 697 | return XYvector.x * X_QR + XYvector.y * Y_QR; 698 | } 699 | 700 | /// 701 | /// Convert a q,r vector to an x,y vector. 702 | /// 703 | public static Vector2 VectorQRtoXY(Vector2 QRvector) 704 | { 705 | return QRvector.x * Q_XY + QRvector.y * R_XY; 706 | } 707 | 708 | /// 709 | /// Get the corners of a QR-space rectangle containing every cell touching an XY-space rectangle. 710 | /// 711 | public static HexCoord[] CartesianRectangleBounds(Vector2 cornerA, Vector2 cornerB) 712 | { 713 | Vector2 min = new Vector2(Math.Min(cornerA.x, cornerB.x), Math.Min(cornerA.y, cornerB.y)); 714 | Vector2 max = new Vector2(Math.Max(cornerA.x, cornerB.x), Math.Max(cornerA.y, cornerB.y)); 715 | HexCoord[] results = { 716 | HexCoord.AtPosition(min), 717 | HexCoord.AtPosition(max) 718 | }; 719 | Vector2 pos = results[0].Position(); 720 | if (pos.y - 0.5f >= min.y) 721 | results[0] += neighbors[4]; 722 | else if (pos.x >= min.x) 723 | results[0] += neighbors[3]; 724 | pos = results[1].Position(); 725 | if (pos.y + 0.5f <= max.y) 726 | results[1] += neighbors[1]; 727 | else if (pos.x <= max.x) 728 | results[1] += neighbors[0]; 729 | return results; 730 | } 731 | 732 | /* 733 | * Operators 734 | */ 735 | 736 | // Cast to Vector2 in QR space. Explicit to avoid QR/XY mix-ups. 737 | public static explicit operator Vector2(HexCoord h) 738 | { return new Vector2(h.q, h.r); } 739 | // +, -, ==, != 740 | public static HexCoord operator +(HexCoord a, HexCoord b) 741 | { return new HexCoord(a.q + b.q, a.r + b.r); } 742 | public static HexCoord operator -(HexCoord a, HexCoord b) 743 | { return new HexCoord(a.q - b.q, a.r - b.r); } 744 | public static bool operator ==(HexCoord a, HexCoord b) 745 | { return a.q == b.q && a.r == b.r; } 746 | public static bool operator !=(HexCoord a, HexCoord b) 747 | { return a.q != b.q || a.r != b.r; } 748 | // Mandatory overrides: Equals(), GetHashCode() 749 | public override bool Equals(object obj) 750 | { return (obj is HexCoord) && this == (HexCoord)obj; } 751 | public override int GetHashCode() 752 | { 753 | return q & (int)0xFFFF | r << 16; 754 | } 755 | 756 | /* 757 | * Constants 758 | */ 759 | 760 | /// 761 | /// One sixth of a full rotation (radians). 762 | /// 763 | public static readonly float SEXTANT = (float)Math.PI / 3; 764 | 765 | /// 766 | /// Square root of 3. 767 | /// 768 | public static readonly float SQRT3 = Mathf.Sqrt(3); 769 | 770 | // The directions array. These are private to prevent overwriting elements. 771 | static readonly HexCoord[] neighbors = { 772 | new HexCoord(1, 0), 773 | new HexCoord(0, 1), 774 | new HexCoord(-1, 1), 775 | new HexCoord(-1, 0), 776 | new HexCoord(0, -1), 777 | new HexCoord(1, -1) 778 | }; 779 | 780 | // Corner locations in XY space. Private for same reason as neighbors. 781 | static readonly Vector2[] corners = { 782 | new Vector2(Mathf.Sin(SEXTANT), Mathf.Cos(SEXTANT)), 783 | new Vector2(0, 1), 784 | new Vector2(Mathf.Sin(-SEXTANT), Mathf.Cos(-SEXTANT)), 785 | new Vector2(Mathf.Sin((float)Math.PI + SEXTANT), Mathf.Cos((float)Math.PI - SEXTANT)), 786 | new Vector2(0, -1), 787 | new Vector2(Mathf.Sin((float)Math.PI - SEXTANT), Mathf.Cos((float)Math.PI - SEXTANT)) 788 | }; 789 | 790 | // Vector transformations between QR and XY space. 791 | // Private to keep IntelliSense tidy. Safe to make public, but sensible uses are covered above. 792 | static readonly Vector2 Q_XY = new Vector2(SQRT3, 0); 793 | static readonly Vector2 R_XY = new Vector2(SQRT3 / 2, 1.5f); 794 | static readonly Vector2 X_QR = new Vector2(SQRT3 / 3, 0); 795 | static readonly Vector2 Y_QR = new Vector2(-1 / 3f, 2 / 3f); 796 | 797 | } 798 | 799 | //work around Godot's lack of implicit Vector2->Vector3 cast 800 | static class Vec2 801 | { 802 | //GODOT: vector2->vector3 cast, vector3 ctor that takes vector2 and z, vector2 ctor that uses one value for both 803 | public static Vector3 ToVec3(this Vector2 vec) => new Vector3(vec.x, vec.y, 0); 804 | //GODOT: static versions of these functions can be useful 805 | public static Vector3 Cross3(this Vector2 v1, Vector2 v2) => new Vector3(v1.x, v1.y, 0).Cross(new Vector3(v2.x, v2.y, 0)); 806 | } 807 | } --------------------------------------------------------------------------------