diff --git a/core/globals/gamestate.gd b/core/globals/gamestate.gd new file mode 100644 index 0000000..ef2dbc1 --- /dev/null +++ b/core/globals/gamestate.gd @@ -0,0 +1,87 @@ +extends Node + +# gamestate + +var last_entrypoint: String + +func reset_gamestate(): + # defaults + last_entrypoint = "intro_start" + +# save/load slot + +const slot_min: int = 0 +const slot_max: int = 3 + +var current_slot: int = -1 +var current_slot_locked: bool = false + +func _check_slot_idx(idx: int) -> bool: + # returns if slot index is valid (in bounds) + if idx < slot_min or idx > slot_max: + push_error("Slot index %s is invalid." % current_slot) + return false + return true + +func save_slot(): + # flush game state to slot + if _check_slot_idx(current_slot): + var slot_path: String = "user://slot%s.save" % current_slot + var f = FileAccess.open(slot_path, FileAccess.WRITE) + if f == null: + push_error("Couldn't open %s" % slot_path) + quit(1) + return + # create serializable data structure + var data: Dictionary = { + "last_entrypoint": last_entrypoint + } + var data_serialized = JSON.stringify(data) + if data_serialized == null: + push_error("Couldn't serialize save data for slot %s" % current_slot) + quit(1) + return + f.store_string(data_serialized) + +func load_slot(): + # load gamestate from slot + if _check_slot_idx(current_slot): + reset_gamestate() # init gamestate with defaults + var slot_path: String = "user://slot%s.save" % current_slot + if FileAccess.file_exists(slot_path): + var f = FileAccess.open(slot_path, FileAccess.READ) + if f == null: + push_error("Couldn't open %s" % slot_path) + quit(1) + return + var data_serialized = f.get_as_text() + var data = JSON.parse_string(data_serialized) + if data == null: + push_error("Couldn't deserialize slot %s" % slot_path) + quit(1) + return + # read values + last_entrypoint = data["last_entrypoint"] + else: + save_slot() # new game; save defaults + +func reset_slot(idx: int): + # reset a save slot + if _check_slot_idx(idx): + if current_slot == idx: + reset_gamestate() + # delete slot file + var slot_path: String = "user://slot%s.save" % idx + if FileAccess.file_exists(slot_path): + DirAccess.remove_absolute(slot_path) + +# + +func quit(code: int): + # defer get_tree().quit() to not lose the last error/stdout + # see https://github.com/godotengine/godot/issues/90667 + await get_tree().create_timer(0.1).timeout + get_tree().quit(code) + +func _ready() -> void: + reset_gamestate() diff --git a/core/globals/levels.gd b/core/globals/levels.gd index 030c189..c7f3921 100644 --- a/core/globals/levels.gd +++ b/core/globals/levels.gd @@ -1,59 +1,79 @@ extends Node +const mainmenu_player_pos: Vector2 = Vector2(0, 128 + 32) + class Entrypoint extends Object: var scene_name: String var player_position: Vector2 - var reset_physics: bool + var initial_velocity: Vector2 func _init( scene_name_: String, player_position_: Vector2, - reset_physics_: bool + initial_velocity_: Vector2 = Vector2.ZERO ) -> void: self.scene_name = scene_name_ self.player_position = player_position_ - self.reset_physics = reset_physics_ + self.initial_velocity = initial_velocity_ const SCENES = { - "intro": "uid://c6w7lrydi43ts" + "intro": "uid://c6w7lrydi43ts", + "test": "uid://dqf665b540tfg", } var ENTRYPOINTS = { - "intro_start": Entrypoint.new("intro", Vector2(0, 0), true) + "intro_start": Entrypoint.new("intro", Vector2.ZERO), + "test": Entrypoint.new("test", Vector2.ZERO), } +var MENU_SCENE: PackedScene = preload("res://menu/menu.tscn") + # load that stuff -var level_root: Node -var player: Node2D +func _pre_load_checks() -> bool: + if NodeRegistry.level_root_container == null: + push_error("Can't load level, level_root is not registered yet.") + return false + if NodeRegistry.player == null: + push_error("Can't load entrypoint, player is not registered yet.") + return false + return true func load_scene(scn_name: String) -> bool: # returns true on success - if level_root == null: - push_error("Can't load level, level_root is not registered yet.") + if not _pre_load_checks(): return false if not scn_name in SCENES: push_error("Level " + scn_name + " doesn't exist.") return false unload_scene() var scn = load(SCENES[scn_name]) - level_root.add_child(scn.instantiate()) + NodeRegistry.level_root_container.add_child(scn.instantiate()) return true func unload_scene(): - for c in level_root.get_children(): - c.queue_free() + if NodeRegistry.level_root_container != null: + for c in NodeRegistry.level_root_container.get_children(): + c.queue_free() func load_entrypoint(ep_name: String) -> bool: # returns true on success if not ep_name in ENTRYPOINTS: push_error("Entrypoint " + ep_name + " doesn't exist.") return false - if player == null: - push_error("Can't load entrypoint, player is not registered yet.") + if not _pre_load_checks(): return false var e: Entrypoint = ENTRYPOINTS[ep_name] if load_scene(e.scene_name): - player.position = e.player_position - if e.reset_physics: - player.reset_physics() + NodeRegistry.player.position = e.player_position + NodeRegistry.player.velocity = e.initial_velocity + Gamestate.last_entrypoint = ep_name + Gamestate.save_slot() # save game return true else: return false + +func load_menu(): + if not _pre_load_checks(): + return false + unload_scene() + NodeRegistry.level_root_container.add_child(MENU_SCENE.instantiate()) + NodeRegistry.player.position = mainmenu_player_pos + return true diff --git a/core/globals/node_registry.gd b/core/globals/node_registry.gd new file mode 100644 index 0000000..df2b79d --- /dev/null +++ b/core/globals/node_registry.gd @@ -0,0 +1,4 @@ +extends Node + +var player: CharacterBody2D +var level_root_container: Node diff --git a/levels/intro.gd b/levels/intro.gd index 1fbee27..aa1ba1f 100644 --- a/levels/intro.gd +++ b/levels/intro.gd @@ -1,5 +1,9 @@ extends Node2D func _on_area_2d_body_entered(body: Node2D) -> void: - if body == Levels.player: - Levels.load_entrypoint("intro_start") + if body == NodeRegistry.player: + NodeRegistry.player.die() + +func _on_next_level_body_entered(body: Node2D) -> void: + if body == NodeRegistry.player: + Levels.load_entrypoint("test") diff --git a/levels/intro.tscn b/levels/intro.tscn index f62fbfe..872b433 100644 --- a/levels/intro.tscn +++ b/levels/intro.tscn @@ -24,4 +24,11 @@ color = Color(1, 0.1, 0, 1) polygon = PackedVector2Array(256, 384, 360, 384, 360, 440, 256, 440) color = Color(1, 0.1, 0, 1) +[node name="next_level" type="Area2D" parent="."] + +[node name="Polygon" parent="next_level" instance=ExtResource("1_cup10")] +polygon = PackedVector2Array(416, 400, 416, 424, 440, 424, 440, 400) +color = Color(0.484431, 0.687354, 1, 1) + [connection signal="body_entered" from="Area2D" to="." method="_on_area_2d_body_entered"] +[connection signal="body_entered" from="next_level" to="." method="_on_next_level_body_entered"] diff --git a/levels/test.tscn b/levels/test.tscn new file mode 100644 index 0000000..96f54bb --- /dev/null +++ b/levels/test.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://dqf665b540tfg"] + +[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_xm2ft"] + +[node name="Test" type="Node2D"] + +[node name="StaticBody2D" type="StaticBody2D" parent="."] + +[node name="Polygon" parent="StaticBody2D" instance=ExtResource("1_xm2ft")] +polygon = PackedVector2Array(-104, 232, -104, 320, 552, 320, 552, 232) diff --git a/main.gd b/main.gd index 0aac732..b575082 100644 --- a/main.gd +++ b/main.gd @@ -1,6 +1,6 @@ extends Node2D func _ready() -> void: - Levels.level_root = $LevelRoot - Levels.player = $Player - Levels.load_entrypoint("intro_start") + NodeRegistry.level_root_container = $LevelRoot + NodeRegistry.player = $Player + Levels.load_menu() diff --git a/menu/menu.gd b/menu/menu.gd new file mode 100644 index 0000000..065949e --- /dev/null +++ b/menu/menu.gd @@ -0,0 +1,5 @@ +extends Node2D + +func _physics_process(_delta: float) -> void: + if NodeRegistry.player.position.y > 1000: + NodeRegistry.player.position = Vector2(0, 0) diff --git a/menu/menu.tscn b/menu/menu.tscn new file mode 100644 index 0000000..157a2ed --- /dev/null +++ b/menu/menu.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=4 format=3 uid="uid://bqmpoix37kutp"] + +[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_8p275"] +[ext_resource type="Script" path="res://menu/menu.gd" id="1_g2w4y"] +[ext_resource type="PackedScene" uid="uid://c40fli7qcma78" path="res://menu/slot/slot.tscn" id="3_rc4dm"] + +[node name="Menu" type="Node2D"] +position = Vector2(0, -1) +script = ExtResource("1_g2w4y") + +[node name="Label" type="Label" parent="."] +offset_left = -192.0 +offset_top = -15.0 +offset_right = 192.0 +offset_bottom = 65.0 +theme_override_font_sizes/font_size = 52 +text = "Main Menu" +horizontal_alignment = 1 +vertical_alignment = 2 + +[node name="StaticBody2D" type="StaticBody2D" parent="."] + +[node name="Polygon" parent="StaticBody2D" instance=ExtResource("1_8p275")] +polygon = PackedVector2Array(-256, 209, 320, 209, 320, 193, -272, 193, -272, 601, -304, 601, -304, 617, -256, 617) + +[node name="SaveGameSlots" type="Node2D" parent="."] +position = Vector2(-448, 177) + +[node name="Saves" type="Label" parent="SaveGameSlots"] +offset_left = -64.0 +offset_top = -176.0 +offset_right = 64.0 +offset_bottom = -112.0 +theme_override_font_sizes/font_size = 42 +text = "Saves" +horizontal_alignment = 1 +vertical_alignment = 2 + +[node name="Slot0" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")] +slot_label = "0" + +[node name="Slot1" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")] +position = Vector2(0, 128) +slot_idx = 1 +slot_label = "1" + +[node name="Slot2" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")] +position = Vector2(0, 256) +slot_idx = 2 +slot_label = "2" + +[node name="Slot3" parent="SaveGameSlots" instance=ExtResource("3_rc4dm")] +position = Vector2(0, 384) +slot_idx = 3 +slot_label = "3" diff --git a/menu/slot/slot.gd b/menu/slot/slot.gd new file mode 100644 index 0000000..dc5cc4c --- /dev/null +++ b/menu/slot/slot.gd @@ -0,0 +1,21 @@ +extends Node2D + +@export var slot_idx: int +@export var slot_label: String = "" + + +func _ready() -> void: + $Label.text = str(slot_label) + +func _on_area_2d_load_body_entered(body: Node2D) -> void: + # load slot & start game + if body == NodeRegistry.player: + Gamestate.current_slot = slot_idx + Gamestate.load_slot() + Levels.load_entrypoint(Gamestate.last_entrypoint) + +func _on_area_2d_delete_body_entered(body: Node2D) -> void: + # reset slot on disk + if body == NodeRegistry.player: + Gamestate.reset_slot(slot_idx) + NodeRegistry.player.position = Levels.mainmenu_player_pos diff --git a/menu/slot/slot.tscn b/menu/slot/slot.tscn new file mode 100644 index 0000000..a2a4149 --- /dev/null +++ b/menu/slot/slot.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=3 format=3 uid="uid://c40fli7qcma78"] + +[ext_resource type="PackedScene" uid="uid://cbynoofsjcl45" path="res://core/polygon.tscn" id="1_2cb7u"] +[ext_resource type="Script" path="res://menu/slot/slot.gd" id="1_3dgi0"] + +[node name="Slot" type="Node2D"] +script = ExtResource("1_3dgi0") + +[node name="StaticBody2D" type="StaticBody2D" parent="."] + +[node name="platform" parent="StaticBody2D" instance=ExtResource("1_2cb7u")] +polygon = PackedVector2Array(44, 16, -44, 16, -44, 48, -64, 48, -64, 16, -80, 16, -80, 56, 80, 56, 80, 16, 64, 16, 64, 48, 44, 48) + +[node name="Area2D_Load" type="Area2D" parent="."] + +[node name="Polygon" parent="Area2D_Load" instance=ExtResource("1_2cb7u")] +polygon = PackedVector2Array(44, 48, 64, 48, 64, 24, 44, 24) +color = Color(0, 1, 0, 1) + +[node name="Area2D_Delete" type="Area2D" parent="."] + +[node name="Polygon" parent="Area2D_Delete" instance=ExtResource("1_2cb7u")] +polygon = PackedVector2Array(-64, 48, -44, 48, -44, 24, -64, 24) +color = Color(1, 0, 0, 1) + +[node name="Label" type="Label" parent="."] +offset_left = -16.0 +offset_top = -32.0 +offset_right = 16.0 +text = "_" +horizontal_alignment = 1 +vertical_alignment = 1 + +[connection signal="body_entered" from="Area2D_Load" to="." method="_on_area_2d_load_body_entered"] +[connection signal="body_entered" from="Area2D_Delete" to="." method="_on_area_2d_delete_body_entered"] diff --git a/player/player.gd b/player/player.gd index 61ac92b..d2730ea 100644 --- a/player/player.gd +++ b/player/player.gd @@ -1,9 +1,16 @@ extends CharacterBody2D +# die + +func die(): + Levels.load_entrypoint(Gamestate.last_entrypoint) + +# movement and stuff @export var movement_speed = 300.0 @export var jump_velocity = -350.0 @export var max_jumps: int = 2 +@export var rigidbody_impulse_mult: float = 0.04 @onready var ceiling_raycast1 = $RayCastUp1 @onready var ceiling_raycast2 = $RayCastUp2 @@ -34,6 +41,12 @@ func _physics_process(delta: float) -> void: velocity.y = jump_velocity jumps += 1 move_and_slide() - -func reset_physics(): - velocity = Vector2.ZERO + # affect rigid bodies + # adapted solution from + # https://kidscancode.org/godot_recipes/4.x/physics/character_vs_rigid/index.html + for i in get_slide_collision_count(): + var collision = get_slide_collision(i) + var collider = collision.get_collider() + if collider is RigidBody2D: + var impulse = -collision.get_normal() * (velocity.length() / collider.mass) * rigidbody_impulse_mult + collider.apply_central_impulse(impulse) diff --git a/project.godot b/project.godot index 13f9ad0..a11148b 100644 --- a/project.godot +++ b/project.godot @@ -19,6 +19,8 @@ config/icon="res://icon.svg" [autoload] Levels="*res://core/globals/levels.gd" +NodeRegistry="*res://core/globals/node_registry.gd" +Gamestate="*res://core/globals/gamestate.gd" [debug] @@ -54,6 +56,8 @@ player_right={ [physics] +common/physics_ticks_per_second=120 +common/max_physics_steps_per_frame=16 common/physics_jitter_fix=0.0 2d/physics_engine="Rapier2D" common/physics_interpolation=true @@ -61,6 +65,7 @@ common/physics_interpolation=true [rendering] environment/defaults/default_clear_color=Color(0, 0, 0, 1) +anti_aliasing/quality/screen_space_aa=1 anti_aliasing/size/mode=3 anti_aliasing/stretch/aspect="keep" anti_aliasing/stretch/mode="viewport"