From fc707b23f741a736c849f948317523e28ebd3914 Mon Sep 17 00:00:00 2001 From: Feufochmar Date: Sun, 7 Nov 2021 21:16:26 +0100 Subject: [PATCH] Add highscores saves and level selection. The levels are not played sequentially, but the level played can be choosen at start. --- main.gd | 36 +++++++++++++++-- screens/game_over_screen.gd | 6 ++- screens/game_screen.gd | 38 +++--------------- screens/level_select_screen.gd | 43 ++++++++++++++++++++ screens/level_select_screen.tscn | 69 ++++++++++++++++++++++++++++++++ screens/start_screen.gd | 2 +- tools/highscores.gd | 58 +++++++++++++++++++++++++++ tools/levels.gd | 54 +++++++++++++++++++++++++ 8 files changed, 266 insertions(+), 40 deletions(-) create mode 100644 screens/level_select_screen.gd create mode 100644 screens/level_select_screen.tscn create mode 100644 tools/highscores.gd create mode 100644 tools/levels.gd diff --git a/main.gd b/main.gd index 3981f46..fe0e21c 100644 --- a/main.gd +++ b/main.gd @@ -1,22 +1,31 @@ extends Node var StartScreen = preload("res://screens/start_screen.tscn") +var LevelSelectScreen = preload("res://screens/level_select_screen.tscn") var GameScreen = preload("res://screens/game_screen.tscn") var GameOverScreen = preload("res://screens/game_over_screen.tscn") var PauseScreen = preload("res://screens/pause_screen.tscn") +var HighScores = preload("res://tools/highscores.gd") +var Levels = preload("res://tools/levels.gd") # Variables var current_screen = null # Current displayed screen var current_state = "" # Current state var game_screen = null # To store the game screen while in pause - +var highscores = HighScores.new() # Object to keep the scores +var levels = Levels.new() # Object to keep the levels func _ready(): + # Read the high scores + highscores.load_scores() + # Go to the start screen goToStartScreen() func _on_next_screen(next): if next == "start": goToStartScreen() + if next == "select": + goToLevelSelectScreen() elif next == "game": goToGameScreen() elif next == "game_won": @@ -38,8 +47,20 @@ func goToStartScreen(): current_screen.connect("next_screen", self, "_on_next_screen") add_child(current_screen) +func goToLevelSelectScreen(): + # Clean up current scene + if current_screen != null: + current_screen.queue_free() + if game_screen != null: + game_screen.queue_free() + game_screen = null + current_state = "select" + current_screen = LevelSelectScreen.instance() + current_screen.init(levels, highscores) + current_screen.connect("next_screen", self, "_on_next_screen") + add_child(current_screen) + func goToGameScreen(): - var start_level = 0 if current_screen != null: current_screen.queue_free() current_state = "game" @@ -49,27 +70,34 @@ func goToGameScreen(): game_screen = null else: current_screen = GameScreen.instance() - current_screen.init(start_level) + current_screen.init(levels.get_current()) current_screen.connect("next_screen", self, "_on_next_screen") add_child(current_screen) func goToGameOverScreen(hasWon): # Retrieve score var score = 0 + var level_name = "" if current_state == "game": score = current_screen.get_score() + level_name = current_screen.get_node("LevelName").text elif game_screen != null: score = game_screen.get_score() + level_name = game_screen.get_node("LevelName").text # Clean up current scene if current_screen != null: current_screen.queue_free() if game_screen != null: game_screen.queue_free() game_screen = null + # Highscores are updated only when a level is won (terminated) + var is_new_high = false + if hasWon: + is_new_high = highscores.set_score(level_name, score) # Prepare screen current_state = "game_over" current_screen = GameOverScreen.instance() - current_screen.init(hasWon, score) + current_screen.init(hasWon, score, is_new_high) current_screen.connect("next_screen", self, "_on_next_screen") add_child(current_screen) diff --git a/screens/game_over_screen.gd b/screens/game_over_screen.gd index b79ff0c..76edd33 100644 --- a/screens/game_over_screen.gd +++ b/screens/game_over_screen.gd @@ -8,8 +8,10 @@ signal next_screen(screen_name) func _ready(): pass -func init(hasWon, score): - if hasWon: +func init(hasWon, score, isNewHigh): + if hasWon && isNewHigh: + $Status.text = "New High Score !" + elif hasWon: $Status.text = "Victory !" else: $Status.text = "Game Over !" diff --git a/screens/game_screen.gd b/screens/game_screen.gd index 669151e..f21875f 100644 --- a/screens/game_screen.gd +++ b/screens/game_screen.gd @@ -3,31 +3,9 @@ extends Node signal next_screen(screen_name) var Ball = preload("res://ball/ball.tscn") -# List of levels -var Levels = [ - # - Classic - # - preload("res://levels/level_01_01.tscn"), # "Breakout" - Disposition from the 1st breakout game - preload("res://levels/level_01_02.tscn"), # "Behind the wall" - Wall block - preload("res://levels/level_01_03.tscn"), # "Building" - Hard blocks - preload("res://levels/level_01_04.tscn"), # "Heart" - Half blocks - preload("res://levels/level_01_05.tscn"), # "Paw" - (no new block) - # - Hex - # - preload("res://levels/level_02_01.tscn"), # "Hive" - Hexagonal blocks - preload("res://levels/level_02_02.tscn"), # "Flowers" - Non-regular hexagons - preload("res://levels/level_02_03.tscn"), # "Leaves" - (no new block) - preload("res://levels/level_02_04.tscn"), # "Aquarium" - Fish blocks (+ Leaves) - preload("res://levels/level_02_05.tscn"), # "Stars" - Star3 blocks - # - Moving/Timing blocks - # - # "Invasion" - Left-right movement - # "Ghosts" - Blinking blocks : block appear / disappear - # "Gems" - Gravity blocks - cf. Boulder Dash - # "Many-Eyed Angel" - Blinking blocks 2 : wall / non-wall - # "Tris" - Falling blocks / appearing at random on the top - ] # Declare member variables here. var ball = null # Ball on the field -var current_level = 0 # Index of the current levels var level = null # Current level var total_score = 0 # Score var paused = false @@ -36,11 +14,10 @@ var paused = false func _ready(): pass -func init(level_index): - current_level = level_index +func init(level_type): total_score = 0 update_score() - new_level() + new_level(level_type) new_ball() # Called every frame. 'delta' is the elapsed time since the previous frame. @@ -63,8 +40,8 @@ func unpause(): $BGM.play() paused = false -func new_level(): - level = Levels[current_level].instance() +func new_level(level_type): + level = level_type.instance() level.connect("level_ended", self, "_on_level_ended") level.connect("score_increased", self, "_on_score_increased") $LevelName.text = level.level_name @@ -105,9 +82,4 @@ func _on_EndLevelTimer_timeout(): ball.disconnect("ball_lost", self, "_on_ball_lost") level.queue_free() ball.queue_free() - current_level = current_level + 1 - if current_level >= Levels.size(): - emit_signal("next_screen", "game_won") - else: - new_level() - new_ball() + emit_signal("next_screen", "game_won") diff --git a/screens/level_select_screen.gd b/screens/level_select_screen.gd new file mode 100644 index 0000000..6d6cf07 --- /dev/null +++ b/screens/level_select_screen.gd @@ -0,0 +1,43 @@ +extends Node + +signal next_screen(screen_name) + +var level_list = null +var highscore_list = null +var shown_level = null + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass + +# Init with the level list +func init(levels, highscores): + level_list = levels + highscore_list = highscores + display_level() + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(_delta): + if Input.is_action_just_pressed('ui_accept'): + emit_signal("next_screen", "game") + if Input.is_action_just_pressed("ui_menu"): + emit_signal("next_screen", "start") + if Input.is_action_just_pressed("ui_left"): + level_list.go_to_prev() + display_level() + if Input.is_action_just_pressed("ui_right"): + level_list.go_to_next() + display_level() + +# Display the level +func display_level(): + if shown_level != null: + shown_level.queue_free() + shown_level = level_list.get_current().instance() + $LevelName.text = shown_level.level_name + var high = highscore_list.get_score(shown_level.level_name) + if high == null: + $Score.text = "-no highscore-" + else: + $Score.text = str(high) + $Level.call_deferred("add_child", shown_level) diff --git a/screens/level_select_screen.tscn b/screens/level_select_screen.tscn new file mode 100644 index 0000000..7cd7a69 --- /dev/null +++ b/screens/level_select_screen.tscn @@ -0,0 +1,69 @@ +[gd_scene load_steps=7 format=2] + +[ext_resource path="res://screens/NovaOval.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://empty_level/empty_level.tscn" type="PackedScene" id=2] +[ext_resource path="res://screens/level_select_screen.gd" type="Script" id=3] + +[sub_resource type="DynamicFont" id=1] +size = 24 +font_data = ExtResource( 1 ) + +[sub_resource type="DynamicFont" id=2] +size = 24 +font_data = ExtResource( 1 ) + +[sub_resource type="DynamicFont" id=3] +size = 50 +font_data = ExtResource( 1 ) + +[node name="LevelSelectScreen" type="Node"] +script = ExtResource( 3 ) + +[node name="EmptyLevel" parent="." instance=ExtResource( 2 )] + +[node name="Score" type="Label" parent="."] +anchor_left = 1.0 +anchor_right = 1.0 +margin_left = -440.0 +margin_right = -72.0 +margin_bottom = 32.0 +custom_fonts/font = SubResource( 1 ) +custom_colors/font_color = Color( 0.552941, 0.960784, 0.505882, 1 ) +text = "SCORE" +align = 2 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LevelName" type="Label" parent="."] +margin_left = 72.0 +margin_right = 440.0 +margin_bottom = 32.0 +custom_fonts/font = SubResource( 2 ) +custom_colors/font_color = Color( 0.552941, 0.960784, 0.505882, 1 ) +text = "NAME" +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Level" type="Node" parent="."] + +[node name="Instructions" type="Label" parent="."] +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +margin_left = -512.0 +margin_top = -125.0 +margin_right = 512.0 +custom_fonts/font = SubResource( 3 ) +text = "Select level with next and right, +then press space" +align = 1 +valign = 1 +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/screens/start_screen.gd b/screens/start_screen.gd index 2a94b89..211f3b4 100644 --- a/screens/start_screen.gd +++ b/screens/start_screen.gd @@ -15,7 +15,7 @@ func init(): # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(_delta): if Input.is_action_just_pressed('ui_accept'): - emit_signal("next_screen", "game") + emit_signal("next_screen", "select") elif Input.is_action_just_pressed("ui_menu"): get_tree().notification(MainLoop.NOTIFICATION_WM_QUIT_REQUEST) diff --git a/tools/highscores.gd b/tools/highscores.gd new file mode 100644 index 0000000..5c09ee1 --- /dev/null +++ b/tools/highscores.gd @@ -0,0 +1,58 @@ +extends Reference + +# Score list. A map level_name -> value +var scores = {} +# Path to score file +const score_file = "user://highscores.json" + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + +# Return the score or null if unset +func get_score(level_name: String): + return scores.get(level_name, null) + +# Set score, return true if a new high score was set, false otherwise +func set_score(level_name: String, new_score: int): + var old_score = get_score(level_name) + if (old_score == null) or (new_score > old_score): + scores[level_name] = new_score + save_scores() + return true + else: + return false + +func save_scores(): + # Serialize the scores in JSON + var serialized = JSON.print(scores) + # Write them to score file + var file = File.new() + var err = file.open(score_file, File.WRITE) + if err == OK: + file.store_string(serialized) + file.close() + else: + print("Error opening file " + score_file + " - Unable to save scores") + +func load_scores(): + # Clear scores + scores.clear() + # Read score file, if it exists + var file = File.new() + if file.file_exists(score_file): + var err = file.open(score_file, File.READ) + if err == OK: + var serialized = file.get_as_text() + file.close() + # Parse JSON + var parsed = JSON.parse(serialized) + # Check if OK + if (parsed.error == OK) && (typeof(parsed.result) == TYPE_DICTIONARY): + # Numeric values read from JSON are float type : must be converted to int + for k in parsed.result.keys(): + scores[k] = int(parsed.result[k]) + else: + print("Error reading file " + score_file + " - Unexpected format") + else: + print("Error opening existing file " + score_file) diff --git a/tools/levels.gd b/tools/levels.gd new file mode 100644 index 0000000..45fc45d --- /dev/null +++ b/tools/levels.gd @@ -0,0 +1,54 @@ +extends Reference + +# List of levels +var list = [ + # - Classic - # + preload("res://levels/level_01_01.tscn"), # "Breakout" - Disposition from the 1st breakout game + preload("res://levels/level_01_02.tscn"), # "Behind the wall" - Wall block + preload("res://levels/level_01_03.tscn"), # "Building" - Hard blocks + preload("res://levels/level_01_04.tscn"), # "Heart" - Half blocks + preload("res://levels/level_01_05.tscn"), # "Paw" - (no new block) + # - Hex - # + preload("res://levels/level_02_01.tscn"), # "Hive" - Hexagonal blocks + preload("res://levels/level_02_02.tscn"), # "Flowers" - Non-regular hexagons + preload("res://levels/level_02_03.tscn"), # "Leaves" - (no new block) + preload("res://levels/level_02_04.tscn"), # "Aquarium" - Fish blocks (+ Leaves) + preload("res://levels/level_02_05.tscn"), # "Stars" - Star3 blocks + # - Moving/Timing blocks - # + # "Invasion" - Left-right movement + # "Ghosts" - Blinking blocks : block appear / disappear + # "Gems" - Gravity blocks - cf. Boulder Dash + # "Many-Eyed Angel" - Blinking blocks 2 : wall / non-wall + # "Tris" - Falling blocks / appearing at random on the top + ] + +# Index for moving in the list +var current_level = 0 + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass + +# Get current level +func get_current(): + return list[current_level] + +# Go to next level (list is circulary) +func go_to_next(): + current_level = current_level + 1 + if current_level >= list.size(): + current_level = 0 + +# Go to previous level (list is circulary) +func go_to_prev(): + current_level = current_level - 1 + if current_level < 0: + current_level = list.size() - 1 + +# Check if current level is last +func current_is_last(): + return current_level == list.size() - 1 + +# Check if current level is first +func current_is_first(): + return current_level == 0