From 09a6140d15ab8d2507e42411d96289c7cfb39e57 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 10 Nov 2025 21:17:55 -0500 Subject: [PATCH] ui; game saving and loading; bad guy movement; damage and death anims --- .../animations/Player Jab 48x48.png.import | 6 - .../player_death/Player Death 64x64.png | Bin 0 -> 1584 bytes .../Player Death 64x64.png.import | 34 +++++ .../player_hurt/Player Hurt 48x48.png | Bin 0 -> 1448 bytes .../player_hurt/Player Hurt 48x48.png.import | 34 +++++ .../player_idle/Player Idle 48x48.png.import | 6 - .../player_jump/player jump 48x48.png.import | 6 - .../player new jump 48x48.png.import | 6 - .../player_land/player land 48x48.png.import | 6 - .../Player Punch 64x64(1).png.import | 6 - .../player_run/player run 48x48.png.import | 6 - .../Player Running Shooting 48x48.png.import | 6 - .../player shoot 2H 48x48.png.import | 6 - ...17-sky-background-godot-course-.png.import | 6 - ...rees-background-godot-course-v1.png.import | 6 - .../backgrounds/forestbackground.png.import | 6 - graphics/effects/14.png.import | 6 - graphics/effects/explosion.png.import | 6 - graphics/effects/orb_red.png.import | 6 - graphics/enemies/slime_green(1).png.import | 6 - .../objects/25-10-27-small-crate.png.import | 6 - graphics/objects/mushroom_32x64.png.import | 6 - graphics/objects/veg_32x32.png.import | 6 - graphics/pickups/coin(1).png.import | 6 - ...18-godot-course-Terrain (32x32).png.import | 6 - ...errain (32x32)-color-exchange-1.png.import | 6 - graphics/tiles/crate-100-custom.png.import | 6 - icon.svg.import | 6 - project.godot | 21 ++- resources/level0_game_save_stats.tres | 23 ++++ resources/level1_game_save_stats.tres | 23 ++++ resources/save_object.gd | 4 + resources/save_object.gd.uid | 1 + scenes/level_1.tscn | 25 ++-- scenes/level_2.tscn | 12 +- scenes/level_3.tscn | 16 +-- scenes/player.tscn | 126 +++++++++++++++++- scenes/slimer.tscn | 12 +- scenes/ui.tscn | 47 +++++++ scripts/game_controller.gd | 45 ++++++- scripts/player.gd | 31 ++++- scripts/scene_manager.gd | 88 ++++++++++++ scripts/slimer.gd | 15 ++- scripts/ui.gd | 15 +++ scripts/ui.gd.uid | 1 + 45 files changed, 534 insertions(+), 183 deletions(-) create mode 100644 graphics/animations/player_death/Player Death 64x64.png create mode 100644 graphics/animations/player_death/Player Death 64x64.png.import create mode 100644 graphics/animations/player_hurt/Player Hurt 48x48.png create mode 100644 graphics/animations/player_hurt/Player Hurt 48x48.png.import create mode 100644 resources/level0_game_save_stats.tres create mode 100644 resources/level1_game_save_stats.tres create mode 100644 resources/save_object.gd create mode 100644 resources/save_object.gd.uid create mode 100644 scenes/ui.tscn create mode 100644 scripts/ui.gd create mode 100644 scripts/ui.gd.uid diff --git a/graphics/animations/Player Jab 48x48.png.import b/graphics/animations/Player Jab 48x48.png.import index 70d5a41..22452ae 100644 --- a/graphics/animations/Player Jab 48x48.png.import +++ b/graphics/animations/Player Jab 48x48.png.import @@ -18,8 +18,6 @@ dest_files=["res://.godot/imported/Player Jab 48x48.png-a092fdd9a110293df667baca compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -27,10 +25,6 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/graphics/animations/player_death/Player Death 64x64.png b/graphics/animations/player_death/Player Death 64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..2e6a1ae0514b237d754893f563c87ba5f95dc109 GIT binary patch literal 1584 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 void: # load in characters @@ -30,8 +35,17 @@ func _ready() -> void: timer.connect("timeout", second_counter) timer.start() +func _process(_delta: float) -> void: + if Input.is_action_just_pressed("save"): + save_game() + pass + if Input.is_action_just_pressed("load"): + load_game() + pass + func second_counter() -> void: time_available -= 1 + timer_updated.emit(time_available) if time_available <= 0: print_debug("You ran out of time! Emitting level_changed signal") #get_tree().call_deferred("change_scene_to_file", levels[current_level]) @@ -63,6 +77,7 @@ func on_coin_collected(_body, coin) -> void: destroyed.emit(coin) func set_total_coins(value) -> void: + coin_collected.emit(value) print_debug("Setting coin total to %s" % str(value)) if value == 0: # You won! @@ -86,13 +101,20 @@ func on_player_slimed(_body, slime) -> void: player_damaged.emit(player_current_health, player_stats.starting_health) print_debug("Player Health: " + str(player_current_health) + " of " + str(player_stats.starting_health)) -func add_enemy_to_level(enemy) -> void: +func handle_player_death() -> void: + level_changed.emit(levels[current_level]) + +func add_enemy_to_level(enemy, _stat = null) -> void: print_debug("GC adding %s to level" % enemy.name) var rand_damage : int = randi() % 10 var this_enemy_stats : Dictionary = { "health": enemy_stats.health, "damage": enemy_stats.melee_damage + rand_damage } + if _stat: + this_enemy_stats.health = _stat.health + this_enemy_stats.damage = _stat.damage + print_debug("This enemy's health is %s and its damage is %s" % [this_enemy_stats.health, this_enemy_stats.damage]) enemies_dict[enemy] = this_enemy_stats func bullet_damage(thing_damaged, _bullet) -> void: @@ -109,3 +131,24 @@ func bullet_damage(thing_damaged, _bullet) -> void: func remove_enemy_from_level(enemy) -> void: enemies_dict.erase(enemy) + +func save_game() -> void: + game_saved.emit(current_level, time_available, player_current_health, enemies_dict) + +func load_game() -> void: + if ResourceLoader.exists("res://resources/level" + str(current_level) + "_game_save_stats.tres"): + var saved_game : SaveObject = load("res://resources/level" + str(current_level) + "_game_save_stats.tres") + enemies_dict.clear() + stash_data = saved_game.game_save + game_loaded.emit(stash_data) + +func stash_game(stash) -> void: + print_debug("Stashing game state") + stash_data = stash + var stash_object = SaveObject.new() + stash_object.game_save = stash + ResourceSaver.save(stash_object, "res://resources/level" + str(current_level) + "_game_save_stats.tres") + +func set_player_health(value: int) -> void: + print_debug("Setting player health to %s" % str(value)) + player_current_health = value diff --git a/scripts/player.gd b/scripts/player.gd index 6a0037e..ff5194b 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -4,6 +4,8 @@ extends CharacterBody2D ## ## Controls the player character in this 2D side-scrolling platformer +signal player_died + enum FaceDirection { LEFT, RIGHT @@ -15,7 +17,9 @@ enum State { FALL, SHOOT_STILL, SHOOT_RUN, - PUNCH + PUNCH, + HURT, + DIE } const JUMP_VELOCITY := -500.0 ## Power applied for jumping @@ -49,14 +53,21 @@ func _ready() -> void: add_child(shoot_cooldown_timer) func _physics_process(delta: float) -> void: - handle_input() + if current_state != State.HURT and current_state != State.DIE: + handle_input() + move_and_slide() handle_movement(delta) - move_and_slide() handle_collisions() update_state() update_animation() update_debug() +func handle_damage(_health, _max_health) -> void: + current_state = State.HURT + +func handle_death() -> void: + current_state = State.DIE + func handle_input() -> void: # Handle jumping. @@ -144,7 +155,7 @@ func handle_collisions() -> void: var collider = right_cast.get_collider() #print_debug("Colliding with %s " % collider.name) if collider is Node and collider is RigidBody2D and collider.is_in_group("punchable"): - print_debug("We have a punch target") + #print_debug("We have a punch target") punch_target = collider punch_enabled = true @@ -201,6 +212,14 @@ func update_state() -> void: await player_sprite.animation_finished current_state = State.IDLE + State.HURT: + await player_sprite.animation_finished + current_state = State.IDLE + + State.DIE: + await player_sprite.animation_finished + player_died.emit() + func update_animation() -> void: match facing: FaceDirection.LEFT: @@ -222,6 +241,10 @@ func update_animation() -> void: player_sprite.play("shoot_run") State.PUNCH: player_sprite.play("punch") + State.HURT: + player_sprite.play("hurt") + State.DIE: + player_sprite.play("die") func update_debug(): %StateLabel.text = "Current state: %s" % State.keys()[current_state] diff --git a/scripts/scene_manager.gd b/scripts/scene_manager.gd index 9dc8aa5..0c018b3 100644 --- a/scripts/scene_manager.gd +++ b/scripts/scene_manager.gd @@ -2,12 +2,18 @@ class_name SceneManager extends Node2D var bullet = preload("res://scenes/bullet.tscn") +var coin_scene = preload("res://scenes/coin.tscn") +var crate_scene = preload("res://scenes/small_crate.tscn") +var enemy_scene = preload("res://scenes/slimer.tscn") var grenade = preload("res://scenes/grenade.tscn") var bullet_array = [] var total_allowed_bullets : int = 7 @onready var coins: Node = $"../Coins" +@onready var crates: Node = $"../Crates" @onready var enemies: Node = $"../Enemies" +@onready var player: Player = $"../Player" +@onready var ui: UI = $"../CanvasLayer/UI" func _ready() -> void: GameController.reset() @@ -23,6 +29,14 @@ func build_level() -> void: # Wire up signals from GameController GameController.level_changed.connect(change_scene) GameController.destroyed.connect(destroy) + GameController.game_saved.connect(on_game_saved) + GameController.game_loaded.connect(on_game_loaded) + GameController.player_damaged.connect(player.handle_damage) + GameController.player_damaged.connect(ui.update_health) + GameController.player_death.connect(player.handle_death) + player.player_died.connect(GameController.handle_player_death) + GameController.timer_updated.connect(ui.update_timer) + GameController.coin_collected.connect(ui.update_coins) func update_enemies() -> void: var total_enemies : int = 0 @@ -82,3 +96,77 @@ func change_scene(level) -> void: func destroy(body) -> void: body.queue_free() + +func on_game_loaded(stash) -> void: + print_debug("Loading level with data %s" % str(stash)) + var player_data = stash.player_data + var coins_data = stash.coins_data + var crates_data = stash.crates_data + var enemies_data = stash.enemies_data + # get rid of existing coins + if coins: + for coin in coins.get_children(): + # remove listeners + coin.tree_exited.disconnect(update_coins) + coins.remove_child(coin) + coin.queue_free() + for coin_transform in coins_data: + var coin_object : Coin = coin_scene.instantiate() + coins.add_child(coin_object) + coin_object.transform = coin_transform + update_coins() + if crates: + for crate in crates.get_children(): + crates.remove_child(crate) + crate.queue_free() + for crate_transform in crates_data: + var crate_object : RigidBody2D = crate_scene.instantiate() + crates.add_child(crate_object) + crate_object.transform = crate_transform + if enemies: + for enemy in enemies.get_children(): + enemies.remove_child(enemy) + enemy.queue_free() + for enemy_dict in enemies_data: + var enemy_object : Slimer = enemy_scene.instantiate() + enemies.add_child(enemy_object) + enemy_object.transform = enemy_dict.position + GameController.add_enemy_to_level(enemy_object, enemy_dict) + player.transform = player_data.position + +func on_game_saved(current_level: int, time_available: int, player_current_health: int, enemies_dict: Dictionary) -> void: + print_debug("Saving game!") + print_debug("Player health = %s" % str(player_current_health)) + print_debug("Player transforms = %s" % str(player.transform)) + var player_data = { + "health": player_current_health, + "position": player.transform, + } + var coin_data : Array ## Coin position(s) + if coins: + for coin in coins.get_children(): + print_debug("This coin is at %s" % str(coin.transform)) + coin_data.push_front(coin.transform) + var crate_data : Array ## Crate position(s) + if crates: + for crate in crates.get_children(): + crate_data.push_front(crate.transform) + var enemy_data : Array ## Array of enemies at savepoint + for enemy_key in enemies_dict: + print_debug("Enemy found at %s" % enemy_key.transform) + print_debug("Enemy health is %s" % enemies_dict[enemy_key].damage) + print_debug("Enemy damage is %s" % enemies_dict[enemy_key].health) + var enemy = { + "position": enemy_key.transform, + "health": enemies_dict[enemy_key].health, + "damage": enemies_dict[enemy_key].damage + } + enemy_data.push_front(enemy) + + var stash = { + "player_data": player_data, + "coins_data": coin_data, + "crates_data": crate_data, + "enemies_data": enemy_data + } + GameController.stash_game(stash) diff --git a/scripts/slimer.gd b/scripts/slimer.gd index d84624c..81f49b0 100644 --- a/scripts/slimer.gd +++ b/scripts/slimer.gd @@ -5,13 +5,26 @@ extends Area2D @onready var left_cast: RayCast2D = $LeftCast @onready var right_down_cast: RayCast2D = $RightDownCast @onready var left_down_cast: RayCast2D = $LeftDownCast +@onready var slime_sprite: AnimatedSprite2D = $SlimeSprite -var speed : int = 100 +var speed : int = 25 var direction : int = 1 signal player_slimed(body, slime) func _process(delta: float) -> void: + if not right_down_cast.is_colliding(): + direction = -1 + slime_sprite.flip_h = true + #if not left_down_cast.is_colliding(): + #direction = 1 + #slime_sprite.flip_h = false + #if not right_cast.is_colliding(): + #direction = -1 + #slime_sprite.flip_h = true + #if not left_cast.is_colliding(): + #direction = 1 + #slime_sprite.flip_h = false position.x += direction * speed * delta func _on_body_entered(body: Node2D) -> void: diff --git a/scripts/ui.gd b/scripts/ui.gd new file mode 100644 index 0000000..28c9644 --- /dev/null +++ b/scripts/ui.gd @@ -0,0 +1,15 @@ +class_name UI +extends Control + +@onready var health_label: Label = $VBoxContainer/HBoxContainer/MarginContainer/HealthLabel +@onready var timer_label: Label = $VBoxContainer/HBoxContainer/MarginContainer2/TimerLabel +@onready var coins_label: Label = $VBoxContainer/HBoxContainer/MarginContainer3/CoinsLabel + +func update_health(current_health, max_health) -> void: + health_label.text = "Health: " + str(current_health) + +func update_timer(time_remaining) -> void: + timer_label.text = "Time: " + str(time_remaining) + +func update_coins(coins_remaining) -> void: + coins_label.text = "Coins: " + str(coins_remaining) diff --git a/scripts/ui.gd.uid b/scripts/ui.gd.uid new file mode 100644 index 0000000..172c062 --- /dev/null +++ b/scripts/ui.gd.uid @@ -0,0 +1 @@ +uid://bpyw8duggqhke