Add a main menu scene (#47)
MainMenu.tscn is now project.godot's entry point: Play / Map Editor (disabled, explains itself - #48 wires it up) / Quit (hidden in web builds), styled like the rest of the game's menus and reusing the starfield shader for its background. Play calls GameState.reset_new_game(), so a second playthrough in the same process starts fresh instead of inheriting whatever the previous run left in the autoload - existing tests still load Main.tscn directly and are unaffected. Part of the #40 conquest-mechanic breakdown.
This commit is contained in:
parent
f14ee656ea
commit
a1fb239a50
7 changed files with 297 additions and 1 deletions
27
GameState.gd
27
GameState.gd
|
|
@ -344,6 +344,33 @@ func reset_progression() -> void:
|
|||
vessel_levels.clear()
|
||||
|
||||
|
||||
# Called from the main menu's Play button (#47): the autoload survives
|
||||
# change_scene_to_file(), so without this a second playthrough in the same
|
||||
# process would start from wherever the last one left off instead of a
|
||||
# fresh game.
|
||||
func reset_new_game() -> void:
|
||||
has_return_position = false
|
||||
surviving_ship_count = BASE_FLEET_SIZE
|
||||
surviving_vessel_types = PackedStringArray()
|
||||
return_world_position = Vector2.ZERO
|
||||
return_camera_zoom = 1.0
|
||||
planet_name = ""
|
||||
planet_id = ""
|
||||
planet_definition_id = "rocky"
|
||||
explored_chunks.clear()
|
||||
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
|
||||
fleet_inventory = INVENTORY.new()
|
||||
buildings.clear()
|
||||
next_building_id = 1
|
||||
stored_vessels.clear()
|
||||
vessel_deliveries.clear()
|
||||
extractors.clear()
|
||||
transporters.clear()
|
||||
planet_capture.clear()
|
||||
reset_progression()
|
||||
reset_formation()
|
||||
|
||||
|
||||
# --- building stock and production queue (#16) -----------------------------
|
||||
|
||||
# Since #12, a building cannot just deduct from the bank at the moment of
|
||||
|
|
|
|||
33
MainMenu.gd
Normal file
33
MainMenu.gd
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
extends Node2D
|
||||
|
||||
@onready var play_button: IconButton = $UI/PlayButton
|
||||
@onready var editor_button: IconButton = $UI/EditorButton
|
||||
@onready var quit_button: IconButton = $UI/QuitButton
|
||||
@onready var status_label: Label = $UI/StatusLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
play_button.pressed.connect(_on_play_pressed)
|
||||
editor_button.pressed.connect(_on_editor_pressed)
|
||||
# #48 wires the real map editor scene up to this button; until then it
|
||||
# stays visible but explains itself instead of silently doing nothing
|
||||
# or pointing at a scene that doesn't exist yet.
|
||||
editor_button.enabled = false
|
||||
quit_button.pressed.connect(_on_quit_pressed)
|
||||
quit_button.visible = not OS.has_feature("web")
|
||||
|
||||
|
||||
func _on_play_pressed() -> void:
|
||||
GameState.reset_new_game()
|
||||
get_tree().change_scene_to_file("res://Main.tscn")
|
||||
|
||||
|
||||
func _on_editor_pressed() -> void:
|
||||
if editor_button.enabled:
|
||||
get_tree().change_scene_to_file("res://PlanetSetupEditor.tscn")
|
||||
return
|
||||
status_label.text = "Map editor coming soon"
|
||||
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
get_tree().quit()
|
||||
1
MainMenu.gd.uid
Normal file
1
MainMenu.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://p7yl0hc43yai
|
||||
139
MainMenu.tscn
Normal file
139
MainMenu.tscn
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://MainMenu.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://IconButton.gd" id="2"]
|
||||
[ext_resource type="Shader" path="res://starfield.gdshader" id="3"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="1"]
|
||||
shader = ExtResource("3")
|
||||
shader_parameter/camera_cell = Vector2(0, 0)
|
||||
shader_parameter/camera_frac = Vector2(0, 0)
|
||||
shader_parameter/camera_zoom = 1.0
|
||||
shader_parameter/viewport_size = Vector2(720, 1280)
|
||||
shader_parameter/cell_size = 60.0
|
||||
shader_parameter/bg_color = Color(0.02, 0.02, 0.08, 1)
|
||||
|
||||
[node name="MainMenu" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="BackgroundLayer" type="CanvasLayer" parent="."]
|
||||
layer = -10
|
||||
|
||||
[node name="Starfield" type="ColorRect" parent="BackgroundLayer"]
|
||||
material = SubResource("1")
|
||||
mouse_filter = 2
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="UI" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="UI"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -300
|
||||
offset_right = 300
|
||||
offset_top = -560
|
||||
offset_bottom = -460
|
||||
theme_override_font_sizes/font_size = 60
|
||||
horizontal_alignment = 1
|
||||
text = "SPACESHIP TEST"
|
||||
|
||||
[node name="PlayButton" type="Control" parent="UI"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -64
|
||||
offset_right = 64
|
||||
offset_top = -300
|
||||
offset_bottom = -172
|
||||
script = ExtResource("2")
|
||||
icon = 2
|
||||
glow_color = Color(0.35, 0.95, 0.55, 1)
|
||||
|
||||
[node name="PlayLabel" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -160
|
||||
offset_right = 160
|
||||
offset_top = -160
|
||||
offset_bottom = -94
|
||||
theme_override_font_sizes/font_size = 32
|
||||
horizontal_alignment = 1
|
||||
text = "Play"
|
||||
|
||||
[node name="EditorButton" type="Control" parent="UI"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -64
|
||||
offset_right = 64
|
||||
offset_top = -20
|
||||
offset_bottom = 108
|
||||
script = ExtResource("2")
|
||||
icon = 0
|
||||
glow_color = Color(0.5, 0.75, 1, 1)
|
||||
|
||||
[node name="EditorLabel" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -200
|
||||
offset_right = 200
|
||||
offset_top = 120
|
||||
offset_bottom = 186
|
||||
theme_override_font_sizes/font_size = 32
|
||||
horizontal_alignment = 1
|
||||
text = "Map Editor"
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="UI"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -260
|
||||
offset_right = 260
|
||||
offset_top = 200
|
||||
offset_bottom = 260
|
||||
theme_override_font_sizes/font_size = 26
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
text = ""
|
||||
|
||||
[node name="QuitButton" type="Control" parent="UI"]
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -64
|
||||
offset_right = 64
|
||||
offset_top = 320
|
||||
offset_bottom = 448
|
||||
script = ExtResource("2")
|
||||
icon = 1
|
||||
glow_color = Color(0.9, 0.35, 0.35, 1)
|
||||
|
||||
[node name="QuitLabel" type="Label" parent="UI"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -160
|
||||
offset_right = 160
|
||||
offset_top = 460
|
||||
offset_bottom = 526
|
||||
theme_override_font_sizes/font_size = 32
|
||||
horizontal_alignment = 1
|
||||
text = "Quit"
|
||||
|
|
@ -11,7 +11,7 @@ config_version=5
|
|||
[application]
|
||||
|
||||
config/name="Spaceship Test"
|
||||
run/main_scene="res://Main.tscn"
|
||||
run/main_scene="res://MainMenu.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
|
|
|
|||
95
tests/test_main_menu.gd
Normal file
95
tests/test_main_menu.gd
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_main_menu.gd
|
||||
# Verifies:
|
||||
# 1. the menu loads with Play/Map Editor/Quit, the editor button starts
|
||||
# disabled and explains itself instead of silently doing nothing
|
||||
# 2. Quit's visibility follows OS.has_feature("web") (this platform is
|
||||
# not a web export, so this run exercises the "visible" branch; the
|
||||
# condition itself is what's under test, not the concrete boolean)
|
||||
# 3. Play resets GameState to a fresh game AND reaches Main.tscn
|
||||
|
||||
var menu: Node
|
||||
var game_state: Node
|
||||
var elapsed: float = 0.0
|
||||
var stage: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
menu = load("res://MainMenu.tscn").instantiate()
|
||||
root.add_child(menu)
|
||||
current_scene = menu
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# A scene added to the tree inside _initialize() has not had its
|
||||
# _ready() flushed yet - see test_planet_setups.gd for the same
|
||||
# hazard - so the menu's @onready buttons aren't valid until a
|
||||
# frame has actually processed.
|
||||
if elapsed < 0.1:
|
||||
return false
|
||||
if menu.editor_button.enabled:
|
||||
return _fail("map editor button is not disabled ahead of #48")
|
||||
if menu.quit_button.visible != (not OS.has_feature("web")):
|
||||
return _fail("quit button visibility did not follow OS.has_feature('web')")
|
||||
|
||||
_press(menu.editor_button)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.1:
|
||||
return false
|
||||
if "coming soon" not in menu.status_label.text.to_lower():
|
||||
return _fail("disabled editor button press did not explain itself")
|
||||
if current_scene != menu:
|
||||
return _fail("pressing the disabled editor button changed scenes")
|
||||
|
||||
# Dirty the game state exactly like an in-progress run would,
|
||||
# so a genuine reset is distinguishable from a coincidental match.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 999)
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
game_state.surviving_ship_count = 1
|
||||
game_state.planet_capture["chunk:1:1"] = {"captured": [true], "conquered": true}
|
||||
|
||||
_press(menu.play_button)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
var on_main: bool = current_scene != null and current_scene.name == "Main"
|
||||
var reset_ok: bool = game_state.rocks == 500 and game_state.buildings.is_empty() \
|
||||
and game_state.surviving_ship_count == GameState.BASE_FLEET_SIZE \
|
||||
and game_state.planet_capture.is_empty()
|
||||
print("on_main=%s reset_ok=%s rocks=%d buildings=%d ship_count=%d" % [
|
||||
on_main, reset_ok, game_state.rocks, game_state.buildings.size(),
|
||||
game_state.surviving_ship_count
|
||||
])
|
||||
if not on_main or not reset_ok:
|
||||
return _fail("Play did not reset to a fresh game before reaching Main.tscn")
|
||||
print("TEST_MAIN_MENU: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _press(button: IconButton) -> void:
|
||||
var down: InputEventScreenTouch = InputEventScreenTouch.new()
|
||||
down.pressed = true
|
||||
button._gui_input(down)
|
||||
var up: InputEventScreenTouch = InputEventScreenTouch.new()
|
||||
up.pressed = false
|
||||
button._gui_input(up)
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_MAIN_MENU: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
1
tests/test_main_menu.gd.uid
Normal file
1
tests/test_main_menu.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgf2ichdb7d4j
|
||||
Loading…
Reference in a new issue