commit cc88dc9f9ed672d5cfc2443363023a5dabcf34ba Author: Feufochmar Date: Sun Dec 19 18:02:05 2021 +0100 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a96203 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.import/ diff --git a/LibertinusSans-Regular.otf b/LibertinusSans-Regular.otf new file mode 100644 index 0000000..e3cc0e8 Binary files /dev/null and b/LibertinusSans-Regular.otf differ diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/entity/chest.tscn b/entity/chest.tscn new file mode 100644 index 0000000..1cec624 --- /dev/null +++ b/entity/chest.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=9 format=2] + +[sub_resource type="CubeMesh" id=1] +size = Vector3( 0.2, 0.2, 0.6 ) + +[sub_resource type="OpenSimplexNoise" id=2] +period = 12.0 +persistence = 0.3 +lacunarity = 4.0 + +[sub_resource type="NoiseTexture" id=3] +height = 8 +noise = SubResource( 2 ) + +[sub_resource type="SpatialMaterial" id=4] +albedo_color = Color( 0.756863, 0.658824, 0.321569, 1 ) +albedo_texture = SubResource( 3 ) + +[sub_resource type="CylinderMesh" id=5] +top_radius = 0.1 +bottom_radius = 0.1 +height = 0.59 + +[sub_resource type="OpenSimplexNoise" id=6] +period = 12.0 +persistence = 0.3 +lacunarity = 4.0 + +[sub_resource type="NoiseTexture" id=7] +height = 8 +bump_strength = 20.0 +noise = SubResource( 6 ) + +[sub_resource type="SpatialMaterial" id=8] +albedo_color = Color( 0.756863, 0.658824, 0.321569, 1 ) +albedo_texture = SubResource( 7 ) + +[node name="Chest" type="Spatial"] + +[node name="Bottom" type="MeshInstance" parent="."] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0 ) +mesh = SubResource( 1 ) +material/0 = SubResource( 4 ) + +[node name="Top" type="MeshInstance" parent="."] +transform = Transform( 1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0.2, 0 ) +mesh = SubResource( 5 ) +material/0 = SubResource( 8 ) diff --git a/entity/entrance.tscn b/entity/entrance.tscn new file mode 100644 index 0000000..7f96f33 --- /dev/null +++ b/entity/entrance.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=3 format=2] + +[sub_resource type="CylinderMesh" id=1] +top_radius = 0.8 +bottom_radius = 0.9 +height = 0.1 + +[sub_resource type="SpatialMaterial" id=2] +albedo_color = Color( 0.913725, 0.929412, 0.341176, 1 ) + +[node name="Entrance" type="Spatial"] + +[node name="MeshInstance" type="MeshInstance" parent="."] +mesh = SubResource( 1 ) +material/0 = SubResource( 2 ) diff --git a/entity/exit.tscn b/entity/exit.tscn new file mode 100644 index 0000000..1e69a4d --- /dev/null +++ b/entity/exit.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=3 format=2] + +[sub_resource type="CylinderMesh" id=1] +top_radius = 0.8 +bottom_radius = 0.9 +height = 0.1 + +[sub_resource type="SpatialMaterial" id=2] +albedo_color = Color( 0.870588, 0.341176, 0.929412, 1 ) + +[node name="Exit" type="Spatial"] + +[node name="MeshInstance" type="MeshInstance" parent="."] +mesh = SubResource( 1 ) +material/0 = SubResource( 2 ) diff --git a/entity/ground.tscn b/entity/ground.tscn new file mode 100644 index 0000000..6dc3992 --- /dev/null +++ b/entity/ground.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=2] + +[sub_resource type="CubeMesh" id=5] +size = Vector3( 100, 2, 100 ) + +[sub_resource type="OpenSimplexNoise" id=2] +octaves = 8 +period = 16.0 +persistence = 0.75 +lacunarity = 3.0 + +[sub_resource type="NoiseTexture" id=3] +width = 64 +height = 64 +seamless = true +noise = SubResource( 2 ) + +[sub_resource type="SpatialMaterial" id=4] +albedo_color = Color( 0.337255, 0.631373, 0.180392, 1 ) +albedo_texture = SubResource( 3 ) +uv1_scale = Vector3( 0.25, 1, 0.25 ) +uv1_triplanar = true + +[node name="Ground" type="Spatial"] + +[node name="MeshInstance" type="MeshInstance" parent="."] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0 ) +mesh = SubResource( 5 ) +material/0 = SubResource( 4 ) diff --git a/entity/player.gd b/entity/player.gd new file mode 100644 index 0000000..8fdf179 --- /dev/null +++ b/entity/player.gd @@ -0,0 +1,85 @@ +class_name Player +extends Spatial + +# Parameters +var speed = 4 # m/s +var spin = PI/2 # rad/s + +# By default, not moving and not turning +var moving = false +var turning = false + +# When moving, these are not null +var moving_vel = null +var target_pos = null +var turning_ang = 0.0 +var orig_ang = 0.0 +var target_ang = 0.0 + +# Move at position +func teleport_at(v: Vector3): + transform.origin = v + Vector3(0, 0.5, 0) + +# Look at a direction +func turn_at(dir): + match dir: + Direction.NORTH: + rotation.y = 0 + Direction.SOUTH: + rotation.y = PI + Direction.EAST: + rotation.y = -PI/2 + Direction.WEST: + rotation.y = PI/2 + +# Indicate if player is moving or turning +func in_movement(): + return moving or turning + +# Move forward +func move_forward(): + if not in_movement(): + moving = true + moving_vel = Vector3.FORWARD + moving_vel *= speed + target_pos = to_global(2 * Vector3.FORWARD) + +func move_backward(): + if not in_movement(): + moving = true + moving_vel = Vector3.BACK + moving_vel *= speed + target_pos = to_global(2 * Vector3.BACK) + +func turn_left(): + if not in_movement(): + turning = true + orig_ang = rotation.y + target_ang = rotation.y + PI/2 + turning_ang = 0.0 + +func turn_right(): + if not in_movement(): + turning = true + orig_ang = rotation.y + target_ang = rotation.y - PI/2 + turning_ang = 0.0 + +# +func _physics_process(delta): + if moving: + if transform.origin.distance_to(target_pos) < 0.01: + # Move at target pos + transform.origin = target_pos + moving = false + else: + var velocity = moving_vel * delta + translate_object_local(velocity) + if turning: + if abs(rotation.y - target_ang) < 0.05: + # set angle + rotation.y = target_ang + turning = false + else: + turning_ang += spin * delta + rotation.y = lerp_angle(orig_ang, target_ang, turning_ang) diff --git a/entity/player.tscn b/entity/player.tscn new file mode 100644 index 0000000..b991664 --- /dev/null +++ b/entity/player.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://entity/player.gd" type="Script" id=1] + +[sub_resource type="ProceduralSky" id=2] +sky_top_color = Color( 0.878431, 0.92549, 0.972549, 1 ) +sky_horizon_color = Color( 0.117647, 0.219608, 0.172549, 1 ) +ground_bottom_color = Color( 0.133333, 0.270588, 0.0196078, 1 ) +ground_horizon_color = Color( 0.133333, 0.270588, 0.0196078, 1 ) +sun_color = Color( 0.811765, 0.686275, 0.109804, 1 ) +sun_latitude = 20.0 +sun_longitude = 180.0 +sun_angle_min = 2.0 + +[sub_resource type="Environment" id=1] +background_mode = 2 +background_sky = SubResource( 2 ) +background_color = Color( 0.301961, 0.458824, 0.454902, 1 ) +ambient_light_color = Color( 1, 1, 1, 1 ) + +[node name="Player" type="Spatial"] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 ) +script = ExtResource( 1 ) + +[node name="Camera" type="Camera" parent="."] +environment = SubResource( 1 ) +current = true +fov = 60.0 +size = 2.0 +far = 8192.0 + +[node name="RayCast_Center" type="RayCast" parent="."] +cast_to = Vector3( 0, 0, -8 ) + +[node name="RayCast_Left" type="RayCast" parent="."] +cast_to = Vector3( -4.25, 0, -6.75 ) + +[node name="RayCast_Right" type="RayCast" parent="."] +cast_to = Vector3( 4.25, 0, -6.75 ) diff --git a/entity/wall.tscn b/entity/wall.tscn new file mode 100644 index 0000000..091e61c --- /dev/null +++ b/entity/wall.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=5 format=2] + +[sub_resource type="CubeMesh" id=1] +size = Vector3( 2, 2, 0.2 ) + +[sub_resource type="OpenSimplexNoise" id=2] +period = 12.0 +persistence = 0.3 +lacunarity = 4.0 + +[sub_resource type="NoiseTexture" id=3] +width = 64 +height = 4 +noise = SubResource( 2 ) + +[sub_resource type="SpatialMaterial" id=4] +albedo_color = Color( 0.760784, 0.501961, 0.215686, 1 ) +albedo_texture = SubResource( 3 ) +uv1_triplanar = true + +[node name="Wall" type="Spatial" groups=["map_block"]] + +[node name="Mesh" type="MeshInstance" parent="."] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0 ) +mesh = SubResource( 1 ) +material/0 = SubResource( 4 ) diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..8d0d071 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,133 @@ +[preset.0] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../exports/Ygdra/Windows/Ygdra.exe" +script_export_mode=1 +script_encryption_key="" + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +binary_format/64_bits=true +binary_format/embed_pck=false +texture_format/bptc=false +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +texture_format/no_bptc_fallbacks=true +codesign/enable=false +codesign/identity="" +codesign/password="" +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PoolStringArray( ) +application/icon="" +application/file_version="" +application/product_version="" +application/company_name="" +application/product_name="Ygdra" +application/file_description="" +application/copyright="" +application/trademarks="" + +[preset.1] + +name="Mac OSX" +platform="Mac OSX" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../exports/Ygdra/MacOS/Ygdra.zip" +script_export_mode=1 +script_encryption_key="" + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +application/name="Ygdra" +application/info="Made with Godot Engine" +application/icon="" +application/identifier="fr.feufochmar.ygdra" +application/signature="" +application/app_category="Games" +application/short_version="1.0" +application/version="1.0" +application/copyright="" +display/high_res=false +privacy/camera_usage_description="" +privacy/microphone_usage_description="" +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false + +[preset.2] + +name="Linux/X11" +platform="Linux/X11" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../exports/Ygdra/Linux/Ygdra.x86_64" +script_export_mode=1 +script_encryption_key="" + +[preset.2.options] + +custom_template/debug="" +custom_template/release="" +binary_format/64_bits=true +binary_format/embed_pck=false +texture_format/bptc=false +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +texture_format/no_bptc_fallbacks=true + +[preset.3] + +name="HTML5" +platform="HTML5" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../exports/Ygdra/HTML/index.html" +script_export_mode=1 +script_encryption_key="" + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +variant/export_type=0 +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=1 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color( 0, 0, 0, 1 ) diff --git a/hud/button-down.svg b/hud/button-down.svg new file mode 100644 index 0000000..0e0eed4 --- /dev/null +++ b/hud/button-down.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + diff --git a/hud/button-down.svg.import b/hud/button-down.svg.import new file mode 100644 index 0000000..9f84137 --- /dev/null +++ b/hud/button-down.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/button-down.svg-19035486a9e6750d8ab8f48ae9262019.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hud/button-down.svg" +dest_files=[ "res://.import/button-down.svg-19035486a9e6750d8ab8f48ae9262019.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/hud/button-left.svg b/hud/button-left.svg new file mode 100644 index 0000000..9348676 --- /dev/null +++ b/hud/button-left.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + diff --git a/hud/button-left.svg.import b/hud/button-left.svg.import new file mode 100644 index 0000000..735e443 --- /dev/null +++ b/hud/button-left.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/button-left.svg-b493beadc1692f5d95f7116825ca3d31.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hud/button-left.svg" +dest_files=[ "res://.import/button-left.svg-b493beadc1692f5d95f7116825ca3d31.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/hud/button-right.svg b/hud/button-right.svg new file mode 100644 index 0000000..ace9771 --- /dev/null +++ b/hud/button-right.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + diff --git a/hud/button-right.svg.import b/hud/button-right.svg.import new file mode 100644 index 0000000..78ffe25 --- /dev/null +++ b/hud/button-right.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/button-right.svg-a7ae82ec2bd2de27e96aafd5ec8a0173.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hud/button-right.svg" +dest_files=[ "res://.import/button-right.svg-a7ae82ec2bd2de27e96aafd5ec8a0173.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/hud/button-up.svg b/hud/button-up.svg new file mode 100644 index 0000000..e00946d --- /dev/null +++ b/hud/button-up.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + diff --git a/hud/button-up.svg.import b/hud/button-up.svg.import new file mode 100644 index 0000000..0a174c6 --- /dev/null +++ b/hud/button-up.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/button-up.svg-7810841ec24551e68b863549473e8b67.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hud/button-up.svg" +dest_files=[ "res://.import/button-up.svg-7810841ec24551e68b863549473e8b67.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/hud/joypad.svg b/hud/joypad.svg new file mode 100644 index 0000000..66c840b --- /dev/null +++ b/hud/joypad.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hud/joypad.svg.import b/hud/joypad.svg.import new file mode 100644 index 0000000..88753b2 --- /dev/null +++ b/hud/joypad.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/joypad.svg-b01353fcdd771ea7c0728dcff1aff6a9.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://hud/joypad.svg" +dest_files=[ "res://.import/joypad.svg-b01353fcdd771ea7c0728dcff1aff6a9.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/hud/map_viewer.gd b/hud/map_viewer.gd new file mode 100644 index 0000000..09e8621 --- /dev/null +++ b/hud/map_viewer.gd @@ -0,0 +1,101 @@ +extends Control + +# Member variables +export var show_unvisited: bool = false +export var ground_visited: Color = Color(0x359235ff) +export var ground_unvisited: Color = Color(0x359235ce) +export var ground_unseen: Color = Color(0x80ced3ce) +export var wall_visited: Color = Color(0x000000ff) +export var wall_unvisited: Color = Color(0x000000ce) +export var player_color: Color = Color(0x2b40efff) +export var chest_color: Color = Color(0xef2b3dff) +export var entrance_color: Color = Color(0xe9ed57ff) +export var exit_color: Color = Color(0xde57edff) + +var level: Maze = null +var player: MazePlayer = null +const cell_size = 15 +const cell_dim = Vector2(cell_size, cell_size) + +func init(lvl: Maze, pl: MazePlayer): + level = lvl + player = pl + update() + +# Drawing function +func _draw(): + for j in range(level.height): + for i in range(level.width): + drawGround(level.at(i, j), i, j) + drawFeature(level.at(i, j), i, j) + for j in range(level.height): + for i in range(level.width): + drawWalls(level.at(i, j), i, j) + drawPlayer() + +func drawGround(c: Cell, i, j): + var color + if c.visited: + color = ground_visited + elif show_unvisited: + color = ground_unvisited + else: + color = ground_unseen + draw_rect( + Rect2( + cell_size * Vector2(i, j), + cell_dim), + color) + +func drawFeature(c: Cell, i, j): + if c.visited or show_unvisited: + var orig = cell_size * Vector2(i, j) + match c.feature: + Cell.Feature.ENTRANCE: + draw_rect(Rect2(orig, cell_dim), entrance_color) + Cell.Feature.EXIT: + draw_rect(Rect2(orig, cell_dim), exit_color) + Cell.Feature.CHEST: + draw_circle(orig + cell_dim * 0.5, cell_size/2, chest_color) + _: + pass + +func drawWalls(c: Cell, i, j): + drawWall(c, Direction.NORTH, i, j) + drawWall(c, Direction.WEST, i, j) + drawWall(c, Direction.SOUTH, i, j) + drawWall(c, Direction.EAST, i, j) + +func drawWall(c: Cell, dir, i, j): + var color + if c.visited: + color = wall_visited + else: + color = wall_unvisited + if (c.visited or show_unvisited) && (c.get_edge(dir) == Cell.Edge.WALL): + var orig = cell_size * Vector2(i, j) + var from = orig + cell_size * Direction.edge_from(dir) + var to = orig + cell_size * Direction.edge_to(dir) + draw_line(from, to, color, 2) + +func drawPlayer(): + var orig = cell_size * player.position + var points = [Vector2(0, 0), Vector2(0, 0), Vector2(0, 0)] + match player.forward_dir: + Direction.NORTH: + points[0] = orig + Vector2(cell_size / 2, 2) + points[1] = orig + Vector2(cell_size - 2, cell_size - 2) + points[2] = orig + Vector2(2, cell_size - 2) + Direction.SOUTH: + points[0] = orig + Vector2(cell_size / 2, cell_size - 2) + points[1] = orig + Vector2(cell_size - 2, 2) + points[2] = orig + Vector2(2, 2) + Direction.EAST: + points[0] = orig + Vector2(cell_size - 2, cell_size / 2) + points[1] = orig + Vector2(2, 2) + points[2] = orig + Vector2(2, cell_size - 2) + Direction.WEST: + points[0] = orig + Vector2(2, cell_size / 2) + points[1] = orig + Vector2(cell_size - 2, 2) + points[2] = orig + Vector2(cell_size - 2, cell_size - 2) + draw_colored_polygon(PoolVector2Array(points), player_color) diff --git a/hud/map_viewer.tscn b/hud/map_viewer.tscn new file mode 100644 index 0000000..51f6bd9 --- /dev/null +++ b/hud/map_viewer.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://hud/map_viewer.gd" type="Script" id=1] + +[node name="MapViewer" type="Control"] +rect_min_size = Vector2( 600, 600 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": true +} +ground_unvisited = Color( 0.129412, 0.34902, 0.129412, 1 ) +ground_unseen = Color( 0.411765, 0.415686, 0.411765, 1 ) +player_color = Color( 0.0666667, 0.0980392, 0.352941, 1 ) diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/main.gd b/main.gd new file mode 100644 index 0000000..3740794 --- /dev/null +++ b/main.gd @@ -0,0 +1,69 @@ +extends Node + +const ExplorationScreen = preload("res://screen/exploration_screen.tscn") +const StartScreen = preload("res://screen/start_screen.tscn") +const MapScreen = preload("res://screen/map_screen.tscn") +const EndScreen = preload("res://screen/end_screen.tscn") + +# Member variables. +var map_width = 40 +var map_height = 40 +var generator = Generator.new() +var level: Maze = null +var player: MazePlayer = MazePlayer.new() +var current_screen: AbstractScreen = null + +# Called when the node enters the scene tree for the first time. +func _ready(): + randomize() + goToStartScreen() + +func new_level(): + # Generate level + level = generator.generate_map(map_width, map_height) + # Teleport player at the entrance + player.teleport_at(level.entrance) + player.turn_at(randi() % 4) + +func on_next_screen(screen): + match screen: + 'start': + goToStartScreen() + 'exploration': + goToExplorationScreen() + 'map': + goToMapScreen() + 'end': + goToEndScreen() + +func clean_up_screen(): + if current_screen != null: + current_screen.queue_free() + +func attach_screen(): + current_screen.connect("next_screen", self, "on_next_screen") + add_child(current_screen) + +func goToStartScreen(): + clean_up_screen() + current_screen = StartScreen.instance() + new_level() + attach_screen() + +func goToExplorationScreen(): + clean_up_screen() + current_screen = ExplorationScreen.instance() + current_screen.init(level, player) + attach_screen() + +func goToMapScreen(): + clean_up_screen() + current_screen = MapScreen.instance() + current_screen.init(level, player) + attach_screen() + +func goToEndScreen(): + clean_up_screen() + current_screen = EndScreen.instance() + current_screen.init(level, player) + attach_screen() diff --git a/main.tscn b/main.tscn new file mode 100644 index 0000000..7cb0d2e --- /dev/null +++ b/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://main.gd" type="Script" id=1] + +[node name="Main" type="Node"] +script = ExtResource( 1 ) diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..a10a248 --- /dev/null +++ b/project.godot @@ -0,0 +1,150 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Node", +"class": "AbstractScreen", +"language": "GDScript", +"path": "res://screen/abstract_screen.gd" +}, { +"base": "Reference", +"class": "Cell", +"language": "GDScript", +"path": "res://tools/cell.gd" +}, { +"base": "Object", +"class": "Direction", +"language": "GDScript", +"path": "res://tools/direction.gd" +}, { +"base": "Reference", +"class": "Generator", +"language": "GDScript", +"path": "res://tools/generator.gd" +}, { +"base": "Reference", +"class": "Maze", +"language": "GDScript", +"path": "res://tools/maze.gd" +}, { +"base": "Reference", +"class": "MazePlayer", +"language": "GDScript", +"path": "res://tools/maze_player.gd" +}, { +"base": "Spatial", +"class": "Player", +"language": "GDScript", +"path": "res://entity/player.gd" +} ] +_global_script_class_icons={ +"AbstractScreen": "", +"Cell": "", +"Direction": "", +"Generator": "", +"Maze": "", +"MazePlayer": "", +"Player": "" +} + +[application] + +config/name="Ygdra" +run/main_scene="res://main.tscn" +config/icon="res://icon.png" + +[input] + +ui_accept={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +, 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,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777221,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777222,"physical_scancode":0,"unicode":0,"echo":false,"script":null) + ] +} +ui_select={ +"deadzone": 0.5, +"events": [ ] +} +ui_cancel={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777220,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777224,"physical_scancode":0,"unicode":0,"echo":false,"script":null) + ] +} +ui_focus_next={ +"deadzone": 0.5, +"events": [ ] +} +ui_focus_prev={ +"deadzone": 0.5, +"events": [ ] +} +ui_page_up={ +"deadzone": 0.5, +"events": [ ] +} +ui_page_down={ +"deadzone": 0.5, +"events": [ ] +} +ui_home={ +"deadzone": 0.5, +"events": [ ] +} +ui_end={ +"deadzone": 0.5, +"events": [ ] +} +ui_next={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":5,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777236,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777218,"physical_scancode":0,"unicode":0,"echo":false,"script":null) + ] +} +ui_prev={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777235,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":true,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777218,"physical_scancode":0,"unicode":0,"echo":false,"script":null) + ] +} +ui_menu={ +"deadzone": 0.5, +"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,"physical_scancode":0,"unicode":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) + ] +} +ui_info={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777244,"physical_scancode":0,"unicode":0,"echo":false,"script":null) + ] +} + +[layer_names] + +3d_physics/layer_1="wall" +3d_physics/layer_2="ground" +3d_physics/layer_3="player" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +quality/driver/driver_name="GLES2" +vram_compression/import_etc=true +vram_compression/import_etc2=false +environment/default_environment="res://default_env.tres" diff --git a/screen/abstract_screen.gd b/screen/abstract_screen.gd new file mode 100644 index 0000000..b539fa9 --- /dev/null +++ b/screen/abstract_screen.gd @@ -0,0 +1,5 @@ +class_name AbstractScreen +extends Node + +# Abstract screen. Only for definition of signals +signal next_screen(screen_name) diff --git a/screen/end_screen.gd b/screen/end_screen.gd new file mode 100644 index 0000000..e268ebb --- /dev/null +++ b/screen/end_screen.gd @@ -0,0 +1,25 @@ +extends AbstractScreen + +# Initialization +func init(lvl: Maze, pl: MazePlayer): + var has_won = lvl.at(pl.position.x, pl.position.y).feature == Cell.Feature.EXIT + if has_won: + $EndCondition.text = "Left the labyrinth." + else: + $EndCondition.text = "Rescued after using an emergency flare." + $NbChests.text = "Found " + str(pl.nb_chests_found) + " chests." + if has_won: + $NbSteps.text = "Reached the exit in " + str(pl.nb_steps) + " steps." + else: + $NbSteps.text = "Gave up after " + str(pl.nb_steps) + " steps." + $MapViewer.init(lvl, pl) + +func _process(_delta): + if Input.is_action_just_pressed("ui_accept"): + emit_signal("next_screen", "start") + if Input.is_action_just_pressed("ui_cancel"): + emit_signal("next_screen", "start") + if Input.is_action_just_pressed("ui_menu"): + emit_signal("next_screen", "start") + if Input.is_action_just_pressed("ui_info"): + emit_signal("next_screen", "start") diff --git a/screen/end_screen.tscn b/screen/end_screen.tscn new file mode 100644 index 0000000..2dd1cd4 --- /dev/null +++ b/screen/end_screen.tscn @@ -0,0 +1,64 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://screen/end_screen.gd" type="Script" id=1] +[ext_resource path="res://hud/map_viewer.tscn" type="PackedScene" id=2] +[ext_resource path="res://LibertinusSans-Regular.otf" type="DynamicFontData" id=3] + +[sub_resource type="DynamicFont" id=1] +size = 30 +font_data = ExtResource( 3 ) + +[node name="EndScreen" type="Node"] +script = ExtResource( 1 ) + +[node name="ColorRect" type="ColorRect" parent="."] +margin_right = 1024.0 +margin_bottom = 600.0 +color = Color( 0.211765, 0.294118, 0.231373, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="MapViewer" parent="." instance=ExtResource( 2 )] +margin_left = 424.0 +margin_right = 1024.0 +margin_bottom = 600.0 +show_unvisited = true + +[node name="EndCondition" type="Label" parent="."] +margin_right = 384.0 +margin_bottom = 64.0 +custom_fonts/font = SubResource( 1 ) +text = "EndCondition" +align = 1 +valign = 1 +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="NbSteps" type="Label" parent="."] +margin_top = 128.0 +margin_right = 384.0 +margin_bottom = 192.0 +custom_fonts/font = SubResource( 1 ) +text = "NbSteps" +align = 1 +valign = 1 +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="NbChests" type="Label" parent="."] +margin_top = 256.0 +margin_right = 384.0 +margin_bottom = 320.0 +custom_fonts/font = SubResource( 1 ) +text = "NbChests" +align = 1 +valign = 1 +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/screen/exploration_screen.gd b/screen/exploration_screen.gd new file mode 100644 index 0000000..21689f5 --- /dev/null +++ b/screen/exploration_screen.gd @@ -0,0 +1,101 @@ +extends AbstractScreen + +const Wall = preload("res://entity/wall.tscn") +const Chest = preload("res://entity/chest.tscn") + +# Member variables +var level: Maze = null +var player: MazePlayer = null +# Dictionary of feature to their 3D representations +var features = {} + +# Initialization +func init(lvl: Maze, pl: MazePlayer): + level = lvl + player = pl + +func _ready(): + # Create walls + for j in range(level.height): + for i in range(level.width): + place_walls(level.at(i, j), i, j) + place_feature(level.at(i, j), i, j) + # Place Entrance + $Root3D/Entrance.transform.origin = map_pos_to_3D_pos(level.entrance.x, level.entrance.y) + # Place Exit + $Root3D/Exit.transform.origin = map_pos_to_3D_pos(level.exit.x, level.exit.y) + # Place player + $Root3D/Player.teleport_at(map_pos_to_3D_pos(player.position.x, player.position.y)) + $Root3D/Player.turn_at(player.forward_dir) + # Update the level state + updateLevelState() + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(_delta): + if (not $Root3D/Player.in_movement()) and (level.at(player.position.x, player.position.y).feature == Cell.Feature.EXIT): + emit_signal("next_screen", "end") + if Input.is_action_just_pressed("ui_menu"): + emit_signal("next_screen", "end") + if Input.is_action_just_pressed("ui_accept"): + emit_signal("next_screen", "map") + if not $Root3D/Player.in_movement(): + if Input.is_action_pressed("ui_up") and player.can_move_forward(level): + player.move_forward() + $Root3D/Player.move_forward() + updateLevelState() + elif Input.is_action_pressed("ui_down") and player.can_move_backward(level): + player.move_backward() + $Root3D/Player.move_backward() + updateLevelState() + elif Input.is_action_pressed("ui_left"): + player.turn_left() + $Root3D/Player.turn_left() + updateLevelState() + elif Input.is_action_pressed("ui_right"): + player.turn_right() + $Root3D/Player.turn_right() + updateLevelState() + +func map_pos_to_3D_pos(i, j): + return Vector3(-(level.width - 1) + 2 * i, 0, -(level.height - 1) + 2 * j) + +func place_walls(c: Cell, i, j): + new_wall(c, Direction.NORTH, i, j) + new_wall(c, Direction.SOUTH, i, j) + new_wall(c, Direction.EAST, i, j) + new_wall(c, Direction.WEST, i, j) + +func new_wall(c: Cell, dir, i, j): + # Compute position and rotation of the element + var offset = Vector3(0.9 * Direction.offset_x(dir), 0, 0.9 * Direction.offset_y(dir)) + var wall_position = map_pos_to_3D_pos(i, j) + offset + var wall_angle = PI/2 * Direction.offset_x(dir) + if c.get_edge(dir) == Cell.Edge.WALL: + var wall = Wall.instance() + wall.translate_object_local(wall_position) + wall.rotate_object_local(Vector3.UP, wall_angle) + $Root3D/Walls.add_child(wall) + +func place_feature(c: Cell, i, j): + if c.feature == Cell.Feature.CHEST: + new_chest(c, i, j) + +func new_chest(c: Cell, i, j): + var chest = Chest.instance() + chest.translate_object_local(map_pos_to_3D_pos(i, j)) + $Root3D/Chests.add_child(chest) + features[Vector2(i, j)] = chest + +func updateHUD(): + $HUD/NbSteps.text = "Steps: " + str(player.nb_steps) + $HUD/NbChests.text = "Chests: " + str(player.nb_chests_found) + +func updateLevelState(): + level.visit(player.position) + var pos = player.position + if (level.at(pos.x, pos.y).feature == Cell.Feature.CHEST): + player.open_chest() + level.at(pos.x, pos.y).feature = Cell.Feature.NONE + features.get(pos).queue_free() + features.erase(pos) + updateHUD() diff --git a/screen/exploration_screen.tscn b/screen/exploration_screen.tscn new file mode 100644 index 0000000..7da5250 --- /dev/null +++ b/screen/exploration_screen.tscn @@ -0,0 +1,95 @@ +[gd_scene load_steps=11 format=2] + +[ext_resource path="res://screen/exploration_screen.gd" type="Script" id=1] +[ext_resource path="res://entity/player.tscn" type="PackedScene" id=2] +[ext_resource path="res://entity/ground.tscn" type="PackedScene" id=3] +[ext_resource path="res://LibertinusSans-Regular.otf" type="DynamicFontData" id=4] +[ext_resource path="res://entity/exit.tscn" type="PackedScene" id=5] +[ext_resource path="res://entity/entrance.tscn" type="PackedScene" id=6] +[ext_resource path="res://hud/button-up.svg" type="Texture" id=7] +[ext_resource path="res://hud/button-down.svg" type="Texture" id=8] + +[sub_resource type="DynamicFont" id=1] +size = 24 +font_data = ExtResource( 4 ) + +[sub_resource type="DynamicFont" id=2] +size = 24 +font_data = ExtResource( 4 ) + +[node name="ExplorationScreen" type="Node"] +script = ExtResource( 1 ) + +[node name="Root3D" type="Spatial" parent="."] + +[node name="Player" parent="Root3D" instance=ExtResource( 2 )] + +[node name="Ground" parent="Root3D" instance=ExtResource( 3 )] + +[node name="Entrance" parent="Root3D" instance=ExtResource( 6 )] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 5 ) + +[node name="Exit" parent="Root3D" instance=ExtResource( 5 )] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 3 ) + +[node name="Walls" type="Spatial" parent="Root3D"] + +[node name="Chests" type="Spatial" parent="Root3D"] + +[node name="HUD" type="Node" parent="."] + +[node name="NbSteps" type="Label" parent="HUD"] +margin_left = 16.0 +margin_top = 528.0 +margin_right = 97.0 +margin_bottom = 551.0 +custom_fonts/font = SubResource( 1 ) +text = "NbSteps" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="NbChests" type="Label" parent="HUD"] +margin_left = 16.0 +margin_top = 560.0 +margin_right = 97.0 +margin_bottom = 583.0 +custom_fonts/font = SubResource( 2 ) +text = "NbChests" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Help" type="Node" parent="HUD"] + +[node name="Button-Stop" type="Sprite" parent="HUD/Help"] +position = Vector2( 855.5, 568 ) +scale = Vector2( 0.5, 0.5 ) +texture = ExtResource( 7 ) + +[node name="Stop" type="Label" parent="HUD/Help"] +margin_left = 887.5 +margin_top = 552.0 +margin_right = 1009.5 +margin_bottom = 580.0 +custom_fonts/font = SubResource( 2 ) +text = "Leave Game" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Button-Map" type="Sprite" parent="HUD/Help"] +position = Vector2( 856, 503.5 ) +scale = Vector2( 0.5, 0.5 ) +texture = ExtResource( 8 ) + +[node name="Map" type="Label" parent="HUD/Help"] +margin_left = 888.0 +margin_top = 487.5 +margin_right = 1010.0 +margin_bottom = 515.5 +custom_fonts/font = SubResource( 2 ) +text = "Show Map" +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/screen/map_screen.gd b/screen/map_screen.gd new file mode 100644 index 0000000..bd5e80a --- /dev/null +++ b/screen/map_screen.gd @@ -0,0 +1,23 @@ +extends AbstractScreen + +# Ready : set the colors of caption according to the used in MapViewer +func _ready(): + $Captions/Entrance.color = $MapViewer.entrance_color + $Captions/Exit.color = $MapViewer.exit_color + $Captions/Path.color = $MapViewer.ground_visited + $Captions/Player.color = $MapViewer.player_color + $Captions/Chest.color = $MapViewer.chest_color + +# Initialization +func init(lvl: Maze, pl: MazePlayer): + $MapViewer.init(lvl, pl) + +func _process(_delta): + if Input.is_action_just_pressed("ui_accept"): + emit_signal("next_screen", "exploration") + if Input.is_action_just_pressed("ui_cancel"): + emit_signal("next_screen", "exploration") + if Input.is_action_just_pressed("ui_menu"): + emit_signal("next_screen", "exploration") + if Input.is_action_just_pressed("ui_info"): + emit_signal("next_screen", "exploration") diff --git a/screen/map_screen.tscn b/screen/map_screen.tscn new file mode 100644 index 0000000..721aba5 --- /dev/null +++ b/screen/map_screen.tscn @@ -0,0 +1,119 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://screen/map_screen.gd" type="Script" id=1] +[ext_resource path="res://hud/map_viewer.tscn" type="PackedScene" id=2] +[ext_resource path="res://LibertinusSans-Regular.otf" type="DynamicFontData" id=3] + +[sub_resource type="DynamicFont" id=1] +size = 24 +font_data = ExtResource( 3 ) + +[node name="MapScreen" type="Node2D"] +script = ExtResource( 1 ) + +[node name="ColorRect" type="ColorRect" parent="."] +margin_right = 1024.0 +margin_bottom = 600.0 +color = Color( 0.211765, 0.294118, 0.231373, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="MapViewer" parent="." instance=ExtResource( 2 )] +margin_left = 212.0 +margin_right = 812.0 +margin_bottom = 600.0 + +[node name="Captions" type="Node" parent="."] + +[node name="Entrance" type="ColorRect" parent="Captions"] +margin_left = 24.0 +margin_top = 48.0 +margin_right = 40.0 +margin_bottom = 64.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="EntranceLbl" type="Label" parent="Captions"] +margin_left = 56.0 +margin_top = 40.0 +margin_right = 178.0 +margin_bottom = 68.0 +custom_fonts/font = SubResource( 1 ) +text = "Entrance" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Exit" type="ColorRect" parent="Captions"] +margin_left = 24.0 +margin_top = 88.0 +margin_right = 40.0 +margin_bottom = 104.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ExitLbl" type="Label" parent="Captions"] +margin_left = 56.0 +margin_top = 80.0 +margin_right = 178.0 +margin_bottom = 108.0 +custom_fonts/font = SubResource( 1 ) +text = "Exit" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Path" type="ColorRect" parent="Captions"] +margin_left = 24.0 +margin_top = 128.0 +margin_right = 40.0 +margin_bottom = 144.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="PathLbl" type="Label" parent="Captions"] +margin_left = 56.0 +margin_top = 120.0 +margin_right = 178.0 +margin_bottom = 148.0 +custom_fonts/font = SubResource( 1 ) +text = "Path" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Player" type="Polygon2D" parent="Captions"] +position = Vector2( 13.3333, 8.00017 ) +scale = Vector2( 0.666667, 0.999999 ) +polygon = PoolVector2Array( 16, 160, 40, 168, 16, 176 ) + +[node name="PlayerLbl" type="Label" parent="Captions"] +margin_left = 56.0 +margin_top = 160.0 +margin_right = 178.0 +margin_bottom = 188.0 +custom_fonts/font = SubResource( 1 ) +text = "Player" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Chest" type="Polygon2D" parent="Captions"] +position = Vector2( 18.6667, 74.6667 ) +scale = Vector2( 0.666667, 0.666667 ) +polygon = PoolVector2Array( 16, 200, 8, 208, 8, 216, 16, 224, 24, 224, 32, 216, 32, 208, 24, 200 ) + +[node name="ChestLbl" type="Label" parent="Captions"] +margin_left = 56.0 +margin_top = 200.0 +margin_right = 178.0 +margin_bottom = 228.0 +custom_fonts/font = SubResource( 1 ) +text = "Chest" +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/screen/start_screen.gd b/screen/start_screen.gd new file mode 100644 index 0000000..e15d955 --- /dev/null +++ b/screen/start_screen.gd @@ -0,0 +1,8 @@ +extends AbstractScreen + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta): + if Input.is_action_just_pressed("ui_menu"): + get_tree().notification(MainLoop.NOTIFICATION_WM_QUIT_REQUEST) + if Input.is_action_just_pressed("ui_accept"): + emit_signal("next_screen", "exploration") diff --git a/screen/start_screen.tscn b/screen/start_screen.tscn new file mode 100644 index 0000000..3d512ce --- /dev/null +++ b/screen/start_screen.tscn @@ -0,0 +1,231 @@ +[gd_scene load_steps=8 format=2] + +[ext_resource path="res://LibertinusSans-Regular.otf" type="DynamicFontData" id=1] +[ext_resource path="res://screen/start_screen.gd" type="Script" id=2] +[ext_resource path="res://hud/joypad.svg" type="Texture" id=3] +[ext_resource path="res://hud/button-up.svg" type="Texture" id=4] +[ext_resource path="res://hud/button-down.svg" type="Texture" id=5] + +[sub_resource type="DynamicFont" id=1] +size = 80 +font_data = ExtResource( 1 ) + +[sub_resource type="DynamicFont" id=2] +size = 26 +font_data = ExtResource( 1 ) + +[node name="StartScreen" type="Node"] +script = ExtResource( 2 ) + +[node name="ColorRect" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color( 0.211765, 0.294118, 0.231373, 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Message" type="Label" parent="."] +anchor_right = 1.0 +margin_bottom = 92.0 +custom_fonts/font = SubResource( 1 ) +text = "Ygdra" +align = 1 +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Controls" type="Control" parent="."] +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +margin_left = -512.0 +margin_top = -280.0 +margin_right = 512.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Joypad" type="Sprite" parent="Controls"] +position = Vector2( 512, 151 ) +texture = ExtResource( 3 ) + +[node name="Label" type="Label" parent="Controls"] +margin_left = 264.0 +margin_top = 93.0 +margin_right = 324.0 +margin_bottom = 128.0 +custom_fonts/font = SubResource( 2 ) +text = "[UP]" +align = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label2" type="Label" parent="Controls"] +margin_left = 240.0 +margin_top = 123.0 +margin_right = 324.0 +margin_bottom = 158.0 +custom_fonts/font = SubResource( 2 ) +text = "[LEFT]" +align = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label3" type="Label" parent="Controls"] +margin_left = 224.0 +margin_top = 152.0 +margin_right = 323.0 +margin_bottom = 187.0 +custom_fonts/font = SubResource( 2 ) +text = "[DOWN]" +align = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label4" type="Label" parent="Controls"] +margin_left = 224.0 +margin_top = 183.0 +margin_right = 323.0 +margin_bottom = 218.0 +custom_fonts/font = SubResource( 2 ) +text = "[RIGHT]" +align = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label5" type="Label" parent="Controls"] +margin_left = 696.0 +margin_top = 93.0 +margin_right = 759.0 +margin_bottom = 128.0 +custom_fonts/font = SubResource( 2 ) +text = "[ESC]" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label6" type="Label" parent="Controls"] +margin_left = 696.0 +margin_top = 123.0 +margin_right = 856.0 +margin_bottom = 158.0 +custom_fonts/font = SubResource( 2 ) +text = "[BACKSPACE]" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label7" type="Label" parent="Controls"] +margin_left = 696.0 +margin_top = 152.0 +margin_right = 795.0 +margin_bottom = 187.0 +custom_fonts/font = SubResource( 2 ) +text = "[SPACE]" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Label8" type="Label" parent="Controls"] +margin_left = 696.0 +margin_top = 183.0 +margin_right = 795.0 +margin_bottom = 218.0 +custom_fonts/font = SubResource( 2 ) +text = "[F1]" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Infos" type="Control" parent="."] +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +margin_top = -196.0 +margin_bottom = 84.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Goal" type="Label" parent="Infos"] +anchor_right = 1.0 +margin_bottom = 99.0 +custom_fonts/font = SubResource( 2 ) +text = "You accidentally stepped on a yellow plate that teleported you into a mysterious labyrinth. +Find the purple plate to exit it. +Someone hid chests there, can you find them too ?" +align = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Buttons" type="Control" parent="Infos"] +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_top = -152.0 +margin_bottom = -16.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Info-Start" type="Control" parent="Infos/Buttons"] +anchor_left = 0.5 +anchor_right = 0.5 +margin_left = -128.0 +margin_right = 128.0 +margin_bottom = 128.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Start" type="Label" parent="Infos/Buttons/Info-Start"] +margin_left = 96.0 +margin_top = 8.0 +margin_right = 221.0 +margin_bottom = 43.0 +custom_fonts/font = SubResource( 2 ) +text = "Start Game" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Button-Start" type="Sprite" parent="Infos/Buttons/Info-Start"] +position = Vector2( 63.5, 23.5 ) +scale = Vector2( 0.5, 0.5 ) +texture = ExtResource( 5 ) + +[node name="Info-Stop" type="Control" parent="Infos/Buttons"] +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +margin_left = -128.0 +margin_top = -128.0 +margin_right = 128.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Stop" type="Label" parent="Infos/Buttons/Info-Stop"] +margin_left = 96.0 +margin_top = 64.0 +margin_right = 217.0 +margin_bottom = 99.0 +custom_fonts/font = SubResource( 2 ) +text = "Quit Game" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Button-Stop" type="Sprite" parent="Infos/Buttons/Info-Stop"] +position = Vector2( 63.5, 79.5 ) +scale = Vector2( 0.5, 0.5 ) +texture = ExtResource( 4 ) diff --git a/tools/cell.gd b/tools/cell.gd new file mode 100644 index 0000000..8f49312 --- /dev/null +++ b/tools/cell.gd @@ -0,0 +1,98 @@ +class_name Cell +extends Reference + +# Abstract cell of labyrinth +# Type of floor +enum Floor { UNKNOWN, PATH, WALL } + +# Edge kind +enum Edge { WALL, NONE, DOOR } + +# Feature kind +enum Feature { NONE, EVENT, ENTRANCE, EXIT, CHEST, MINE_POINT, TAKE_POINT, CHOP_POINT, HARVEST_POINT } + +### +# A cell has : +# - a kind of floor indicating if it is a path, a wall, ... +var floor_kind = Floor.UNKNOWN +# - a list of edges indicating to which adjacent cell it is connected +var edges = [Edge.WALL, Edge.WALL, Edge.WALL, Edge.WALL] +# - an optional feature : entrance, exit, ... +var feature = Feature.NONE +# - a flag indicating if the cell was visited by the player +var visited = false + +### +# Edge-related functions + +# Set an edge +func set_edge(dir, val): + edges[dir] = val +# Get an edge +func get_edge(dir): + return edges[dir] + +# Deadend +func is_deadend(): + # if there are 3 walls among the edges, the cell is a deadend + var count = 0 + for e in edges: + if e == Edge.WALL: + count += 1 + return count == 3 + +# Utilities for labyrinth-building (transformations) +# Identity : do nothing +func identity(): + pass + +# Horizontal flip : exchange the EAST/WEST edges +func horizontal_flip(): + var tmp = edges[Direction.WEST] + edges[Direction.WEST] = edges[Direction.EAST] + edges[Direction.EAST] = tmp + +# Vertical flip : exchange the NORTH/SOUTH edges +func vertical_flip(): + var tmp = edges[Direction.SOUTH] + edges[Direction.SOUTH] = edges[Direction.NORTH] + edges[Direction.NORTH] = tmp + +# Rotation 180° = horizontal flip + vertical flip +func rotate_180(): + self.horizontal_flip() + self.vertical_flip() + +# Rotation 90° in trigonometric direction (counter-clockwise) +func rotate_90(): + var tmp = edges[Direction.NORTH] + edges[Direction.NORTH] = edges[Direction.EAST] + edges[Direction.EAST] = edges[Direction.SOUTH] + edges[Direction.SOUTH] = edges[Direction.WEST] + edges[Direction.WEST] = tmp + +# Rotation 270° in trigonometric direction (counter-clockwise), or 90° clockwise +func rotate_270(): + var tmp = edges[Direction.NORTH] + edges[Direction.NORTH] = edges[Direction.WEST] + edges[Direction.WEST] = edges[Direction.SOUTH] + edges[Direction.SOUTH] = edges[Direction.EAST] + edges[Direction.EAST] = tmp + +# Transposition (mirror -45°) : exchange NORTH/WEST, SOUTH/EAST +func transpose(): + var tmp = edges[Direction.NORTH] + edges[Direction.NORTH] = edges[Direction.WEST] + edges[Direction.WEST] = tmp + tmp = edges[Direction.SOUTH] + edges[Direction.SOUTH] = edges[Direction.EAST] + edges[Direction.EAST] = tmp + +# Anti-transposition (mirror 45°): exchange NORTH/EAST, SOUTH/WEST +func antitranspose(): + var tmp = edges[Direction.NORTH] + edges[Direction.NORTH] = edges[Direction.EAST] + edges[Direction.EAST] = tmp + tmp = edges[Direction.SOUTH] + edges[Direction.SOUTH] = edges[Direction.WEST] + edges[Direction.WEST] = tmp diff --git a/tools/direction.gd b/tools/direction.gd new file mode 100644 index 0000000..86b1c06 --- /dev/null +++ b/tools/direction.gd @@ -0,0 +1,105 @@ +class_name Direction +extends Object + +# Direction enum, used to refer to the cell edges +enum { NORTH = 0, SOUTH = 1, EAST = 2, WEST = 3 } + +# Get the opposite direction +static func opposite(d): + match d: + NORTH: + return SOUTH + SOUTH: + return NORTH + EAST: + return WEST + WEST: + return EAST + +# Get the offset (X) +static func offset_x(d): + match d: + NORTH: + return 0 + SOUTH: + return 0 + EAST: + return 1 + WEST: + return -1 + +# Get the offset (Y 2D, Z 3D) +static func offset_y(d): + match d: + NORTH: + return -1 + SOUTH: + return 1 + EAST: + return 0 + WEST: + return 0 + +# Get the offset as a vector +static func offset(d): + return Vector2(offset_x(d), offset_y(d)) + + +# Edge offset (from), as unit vectors +static func edge_from(d): + match d: + NORTH: + return Vector2(0, 0) + SOUTH: + return Vector2(0, 1) + EAST: + return Vector2(1, 0) + WEST: + return Vector2(0, 0) + +static func edge_to(d): + match d: + NORTH: + return Vector2(1, 0) + SOUTH: + return Vector2(1, 1) + EAST: + return Vector2(1, 1) + WEST: + return Vector2(0, 1) + +# Rotate left +static func rotate_left(d): + match d: + NORTH: + return WEST + SOUTH: + return EAST + EAST: + return NORTH + WEST: + return SOUTH + +# Rotate right +static func rotate_right(d): + match d: + NORTH: + return EAST + SOUTH: + return WEST + EAST: + return SOUTH + WEST: + return NORTH + +# To string +static func string_of(d): + match d: + NORTH: + return "NORTH" + SOUTH: + return "SOUTH" + EAST: + return "EAST" + WEST: + return "WEST" diff --git a/tools/generator.gd b/tools/generator.gd new file mode 100644 index 0000000..204aad9 --- /dev/null +++ b/tools/generator.gd @@ -0,0 +1,118 @@ +class_name Generator +extends Reference + +# Maze carver position +class MazeCarverPosition: + var x: int = 0 + var y: int = 0 + var available_directions: Array = [Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST] + + func _init(pos_x: int, pos_y: int): + x = pos_x + y = pos_y + available_directions.shuffle() + + func has_next_direction(): + return not available_directions.empty() + + func next_direction(): + return available_directions.pop_back() + +# Generate a new map +# Each map is a array indicating the features of the map +func generate_map(width: int, height: int): + var maze = null + # A correct maze needs at least two events to place the entrance and exit + var events = [] + while events.size() < 2: + maze = Maze.new(width, height) + var start_x = randi() % width + var start_y = randi() % height + carve_maze(maze, start_x, start_y, true) + add_events(maze) + events = maze.events + # Shuffle the event list + maze.events.shuffle() + # Set the entrance and exit + set_entrance(maze) + set_exit(maze) + # Set the chests + set_chests(maze) + return maze + +# Carve a maze (iterative) +func carve_maze(maze: Maze, start_x: int, start_y: int, loop: bool): + # Starting position + var start_pos: MazeCarverPosition = MazeCarverPosition.new(start_x, start_y) + # Position pile + var carver = [start_pos] + # While there is a pos in carver + while not carver.empty(): + var pos: MazeCarverPosition = carver.back() + # Set the current cell floor kind to PATH + maze.at(pos.x, pos.y).floor_kind = Cell.Floor.PATH + # If there are available directions, pick the first, and continue from there + if pos.has_next_direction(): + # Get the next direction + var dir = pos.next_direction() + # Position of next cell + var nx = pos.x + Direction.offset_x(dir) + var ny = pos.y + Direction.offset_y(dir) + # if the next cell is inside the maze, and has an unknown floor kind, + # remove the wall between the cells, and continue from next cell. + if maze.is_inside(nx, ny) and (maze.at(nx, ny).floor_kind == Cell.Floor.UNKNOWN): + maze.at(pos.x, pos.y).set_edge(dir, Cell.Edge.NONE) + maze.at(nx, ny).set_edge(Direction.opposite(dir), Cell.Edge.NONE) + # Push the new position into the carver + var new_pos: MazeCarverPosition = MazeCarverPosition.new(nx, ny) + carver.push_back(new_pos) + # if the next cell is inside the maze and has a path floor kind, + # if the loop is true and at a 1/20 chance, remove the wall, + # but don't continue on the next cell + elif maze.is_inside(nx, ny) and (maze.at(nx, ny).floor_kind == Cell.Floor.PATH) and loop and (randi() % 20 < 1): + maze.at(pos.x, pos.y).set_edge(dir, Cell.Edge.NONE) + maze.at(nx, ny).set_edge(Direction.opposite(dir), Cell.Edge.NONE) + else: + # do nothing + pass + else: + # No new available position : pop pos from carver + carver.pop_back() + +# Add events at the deadends +func add_events(maze: Maze): + for j in range(maze.height): + for i in range(maze.width): + if maze.at(i, j).is_deadend() and (maze.at(i, j).feature == Cell.Feature.NONE): + maze.at(i, j).feature = Cell.Feature.EVENT + maze.events.push_back(Vector2(i, j)) + +# Choose an event to set as the entrance +func set_entrance(maze: Maze): + var pos: Vector2 = maze.events.pop_back() + maze.at(pos.x, pos.y).feature = Cell.Feature.ENTRANCE + maze.entrance = pos + +# Set the exit on the event that is farthest from the entrance +func set_exit(maze: Maze): + var distance = 0 + var exit = maze.entrance + for e in maze.events: + var d = maze.entrance.distance_squared_to(e) + if d > distance: + distance = d + exit = e + maze.at(exit.x, exit.y).feature = Cell.Feature.EXIT + maze.exit = exit + +# Set chests on a few events +func set_chests(maze: Maze): + var max_nb_chests = sqrt(maze.width * maze.height) + var count = 0 + for e in maze.events: + if maze.at(e.x, e.y).feature == Cell.Feature.EVENT: + count += 1 + maze.at(e.x, e.y).feature = Cell.Feature.CHEST + if count >= max_nb_chests: + break + maze.nb_chests = count diff --git a/tools/maze.gd b/tools/maze.gd new file mode 100644 index 0000000..bac7e14 --- /dev/null +++ b/tools/maze.gd @@ -0,0 +1,154 @@ +class_name Maze +extends Reference + +# Represents an area of cell +var width: int = 0 +var height: int = 0 +var cells = [] +var events = [] +var entrance = Vector2() +var exit = Vector2() +var nb_chests: int = 0 +# For submazes, as they modify their parent maze +var parent: Maze = null # Submaze parent maze +var offset_x = 0 +var offset_y = 0 + +# Initialize a maze +func _init(w: int, h: int): + assert(w > 0) + assert(h > 0) + width = w + height = h + cells.clear() + cells.resize(w * h) + for i in range(w * h): + cells[i] = Cell.new() + +# Initialize a submaze from a parent maze. +func init_from(w: int, h: int, that: Maze, x: int, y: int): + assert(w > 0) + assert(h > 0) + width = w + height = h + parent = that + offset_x = x + offset_y = y + +# Get the index for the cell at position (x,y) +func index(x: int, y: int): + return x + y * width + +# Get the cell at the position (x,y) +func at(x: int, y: int): + if parent == null: + return cells[index(x, y)] + else: + parent.at(x + offset_x, y + offset_y) + +# Check if a position is inside the area +func is_inside(x: int, y: int): + return (x >= 0) and (x < width) and (y >= 0) and (y < height) + +# Get a submaze of this maze. +# The submaze if a view of this maze, so a modification of the submaze cells modify this maze. +func submaze(x: int, y: int, w: int, h: int): + assert(x > 0) + assert(y > 0) + assert((x + w) <= width) + assert((y + h) <= height) + var ret = load("res://tools/maze.gd").new() + ret.init_from(w, h, self, x, y) + return ret + +# Transformations +# Identity : do nothing +func identity(_x: int = 0, _y: int = 0, _w: int = width, _h: int = height): + pass + +# Horizontal flip +func horizontal_flip(x: int = 0, y: int = 0, w: int = width, h: int = height): + if parent == null: + for j in range(0, h): + for i in range(0, w/2): + var idx_cell = index(x + i, y + j) + var idx_other = index(x + w - 1 - i, y + j) + var cell = cells[idx_cell] + cells[idx_cell] = cells[idx_other] + cells[idx_other] = cell + for j in range(0, h): + for i in range(0, w): + var idx_cell = index(x + i, y + j) + cells[idx_cell].horizontal_flip() + else: + parent.horizontal_flip(offset_x + x, offset_y + y, w, h) + +# Vertical flip +func vertical_flip(x: int = 0, y: int = 0, w: int = width, h: int = height): + if parent == null: + for j in range(0, h/2): + for i in range(0, w): + var idx_cell = index(x + i, y + j) + var idx_other = index(x + i, y + h - 1 - j) + var cell = cells[idx_cell] + cells[idx_cell] = cells[idx_other] + cells[idx_other] = cell + for j in range(0, h): + for i in range(0, w): + var idx_cell = index(x + i, y + j) + cells[idx_cell].vertical_flip() + else: + parent.vertical_flip(offset_x + x, offset_y + y, w, h) + +# Rotation 180° = horizontal flip + vertical flip +func rotate_180(x: int = 0, y: int = 0, w: int = width, h: int = height): + self.horizontal_flip(x, y, w, h) + self.vertical_flip(x, y, w, h) + +# Rotation 90° in trigonometric direction (counter-clockwise) +func rotate_90(): + # TODO + pass + +# Rotation 270° in trigonometric direction (counter-clockwise), or 90° clockwise +func rotate_270(): + # TODO + pass + +# Transposition (mirror -45°) +func transpose(x: int = 0, y: int = 0, w: int = width, h: int = height): + if parent == null: + for j in range(0, h): + for i in range(j + 1, w): + var idx_cell = index(x + i, y + j) + var idx_other = index(y + j, x + i) + var cell = cells[idx_cell] + cells[idx_cell] = cells[idx_other] + cells[idx_other] = cell + for j in range(0, h): + for i in range(0, w): + var idx_cell = index(x + i, y + j) + cells[idx_cell].transpose() + else: + parent.transpose(offset_x + x, offset_y + y, w, h) + +# Anti-transposition (mirror 45°) +func antitranspose(x: int = 0, y: int = 0, w: int = width, h: int = height): + if parent == null: + for j in range(0, h): + for i in range(0, w - 1 - j): + var idx_cell = index(x + i, y + j) + var idx_other = index(y + h - 1 - j, x + w - 1 - i) + var cell = cells[idx_cell] + cells[idx_cell] = cells[idx_other] + cells[idx_other] = cell + for j in range(0, h): + for i in range(0, w): + var idx_cell = index(x + i, y + j) + cells[idx_cell].antitranspose() + else: + parent.antitranspose(offset_x + x, offset_y + y, w, h) + +# Player-interaction functions +func visit(pos: Vector2): + at(pos.x, pos.y).visited = true diff --git a/tools/maze_player.gd b/tools/maze_player.gd new file mode 100644 index 0000000..7dc4faa --- /dev/null +++ b/tools/maze_player.gd @@ -0,0 +1,40 @@ +class_name MazePlayer +extends Reference + +# Player in maze coordinates +var position = Vector2.ZERO +var forward_dir = Direction.NORTH +var nb_steps = 0 +var nb_chests_found = 0 + +func teleport_at(pos: Vector2): + position = pos + +func turn_at(dir): + forward_dir = dir + +func move_forward(): + position += Direction.offset(forward_dir) + nb_steps += 1 + +func move_backward(): + position += Direction.offset(Direction.opposite(forward_dir)) + nb_steps += 1 + +func turn_left(): + forward_dir = Direction.rotate_left(forward_dir) + +func turn_right(): + forward_dir = Direction.rotate_right(forward_dir) + +# Check if a player can move forward +func can_move_forward(level: Maze): + return level.at(position.x, position.y).get_edge(forward_dir) != Cell.Edge.WALL + +# Check if a player can move backward +func can_move_backward(level: Maze): + return level.at(position.x, position.y).get_edge(Direction.opposite(forward_dir)) != Cell.Edge.WALL + +# Find a chest +func open_chest(): + nb_chests_found += 1