2f6607af8c
Feature work (CLAUDE.md items 17-18): - Race roster art rework (Apex/ISN/ORC) plus per-ship engine-flame sprites that swap in on thrust - Floating damage-number VFX, per-ship nameplates, energy bar rework - Background music manager and per-race ship SFX Bugfixes/tuning from live testing: - No friendly fire: bullets stop on a teammate's hull but deal no damage (bullet.gd), decided by race via PlayerRegistry - Ships no longer physically block each other (own collision layer, ship.tscn/bullet.tscn) -- fixes ramming/parking trolling and the stuck-on- corpse bug after a kill - Explosions ghost any ship's sprite passing through instead of blocking it (effects/explosion.gd's GhostArea) - Fixed two "flushing physics queries" crashes from the above: explosion spawn and death/respawn collision toggling are now deferred (ship_movement.gd) - Dead ships stop blocking bullets during their respawn delay - Bots fly a mixed roster (1 Tank + 2 Gunner + rest Fighter per team) instead of always the Fighter (bot_manager.gd) - Camera zoom knob, larger/longer-lived damage numbers and nameplate text, red (not yellow) enemy name color for better contrast Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
409 lines
12 KiB
GDScript
409 lines
12 KiB
GDScript
extends Control
|
|
|
|
# HL2-style menu: no big mode tiles, just a plain vertical text nav over a
|
|
# space backdrop. Quick Play always queues casual — ranked is gone until the
|
|
# matchmaking/MMR work in overview/task.md lands.
|
|
|
|
var _status_lbl: Label
|
|
var _name_lbl: Label
|
|
var _rank_lbl: Label
|
|
|
|
var _profile_overlay: Control
|
|
var _profile_input: LineEdit
|
|
var _profile_hint_lbl: Label
|
|
|
|
var _connecting: bool = false
|
|
var _pending_quick_play: bool = false # profile overlay was forced open by Quick Play
|
|
|
|
const RANK_DATA := [
|
|
{"label": "CADET", "color": Color(0.72, 0.48, 0.22)},
|
|
{"label": "PILOT", "color": Color(0.78, 0.78, 0.82)},
|
|
{"label": "ACE", "color": Color(1.0, 0.82, 0.22)},
|
|
{"label": "COMMANDER", "color": Color(0.35, 0.88, 1.0)},
|
|
{"label": "ADMIRAL", "color": Color(0.62, 0.28, 1.0)},
|
|
{"label": "LEGEND", "color": Color(1.0, 0.28, 0.10)},
|
|
]
|
|
const CURRENT_RANK := 0 # placeholder until backend exists
|
|
const CURRENT_LEVEL := 1 # placeholder until backend exists
|
|
|
|
const NAV_ITEMS := ["QUICK PLAY", "SERVER SELECT", "OPTIONS", "PROFILE", "QUIT"]
|
|
const NAV_LEFT := 64.0
|
|
const NAV_WIDTH := 360.0
|
|
const NAV_TOP := 250.0
|
|
const NAV_BTN_H := 52.0
|
|
const NAV_GAP := 4.0
|
|
|
|
|
|
func _ready() -> void:
|
|
_build_ui()
|
|
MusicManager.play_menu_music()
|
|
NetworkManager.connection_succeeded.connect(_on_connected)
|
|
NetworkManager.connection_failed.connect(_on_connect_failed)
|
|
MatchmakingClient.match_found.connect(_on_match_found)
|
|
MatchmakingClient.search_failed.connect(_on_search_failed)
|
|
|
|
|
|
func _build_ui() -> void:
|
|
_add_bg()
|
|
_add_logo()
|
|
_add_nav_menu()
|
|
_add_profile_badge()
|
|
_add_status_label()
|
|
_add_version_label()
|
|
_add_profile_overlay()
|
|
_refresh_profile_badge()
|
|
|
|
|
|
func _add_bg() -> void:
|
|
var bg := TextureRect.new()
|
|
bg.texture = load("res://assets/images/background/skybox/2.png")
|
|
bg.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
|
bg.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
|
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(bg)
|
|
|
|
var ov := ColorRect.new()
|
|
ov.color = Color(0.0, 0.0, 0.0, 0.55)
|
|
ov.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(ov)
|
|
|
|
|
|
func _add_logo() -> void:
|
|
var title := Label.new()
|
|
title.text = "S P A C E W A R"
|
|
title.anchor_left = 0.0
|
|
title.anchor_top = 0.0
|
|
title.anchor_right = 0.0
|
|
title.anchor_bottom = 0.0
|
|
title.offset_left = 60.0
|
|
title.offset_top = 56.0
|
|
title.offset_right = 700.0
|
|
title.offset_bottom = 118.0
|
|
title.add_theme_font_size_override("font_size", 44)
|
|
title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
|
add_child(title)
|
|
|
|
var sub := Label.new()
|
|
sub.text = "FIGHT FOR THE GALAXY"
|
|
sub.anchor_left = 0.0
|
|
sub.anchor_top = 0.0
|
|
sub.anchor_right = 0.0
|
|
sub.anchor_bottom = 0.0
|
|
sub.offset_left = 64.0
|
|
sub.offset_top = 116.0
|
|
sub.offset_right = 500.0
|
|
sub.offset_bottom = 138.0
|
|
sub.add_theme_font_size_override("font_size", 13)
|
|
sub.add_theme_color_override("font_color", Color(0.7, 0.72, 0.78, 0.85))
|
|
add_child(sub)
|
|
|
|
|
|
func _add_nav_menu() -> void:
|
|
for i in NAV_ITEMS.size():
|
|
var label_text: String = NAV_ITEMS[i]
|
|
var stub := label_text == "OPTIONS"
|
|
var btn := _make_nav_btn(label_text, stub)
|
|
btn.anchor_left = 0.0
|
|
btn.anchor_top = 0.0
|
|
btn.anchor_right = 0.0
|
|
btn.anchor_bottom = 0.0
|
|
btn.offset_left = NAV_LEFT
|
|
btn.offset_right = NAV_LEFT + NAV_WIDTH
|
|
btn.offset_top = NAV_TOP + i * (NAV_BTN_H + NAV_GAP)
|
|
btn.offset_bottom = btn.offset_top + NAV_BTN_H
|
|
add_child(btn)
|
|
|
|
match label_text:
|
|
"QUICK PLAY":
|
|
btn.pressed.connect(_on_quick_play)
|
|
"SERVER SELECT":
|
|
btn.pressed.connect(_on_server_select)
|
|
"OPTIONS":
|
|
btn.pressed.connect(_on_options)
|
|
"PROFILE":
|
|
btn.pressed.connect(_on_profile)
|
|
"QUIT":
|
|
btn.pressed.connect(_on_quit)
|
|
|
|
|
|
func _make_nav_btn(label_text: String, stub: bool) -> Button:
|
|
var btn := Button.new()
|
|
btn.text = label_text
|
|
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
btn.add_theme_font_size_override("font_size", 22)
|
|
|
|
var sn := StyleBoxEmpty.new()
|
|
sn.content_margin_left = 20.0
|
|
btn.add_theme_stylebox_override("normal", sn)
|
|
|
|
if stub:
|
|
btn.add_theme_color_override("font_color", Color(0.4, 0.42, 0.48, 0.8))
|
|
btn.add_theme_stylebox_override("hover", sn)
|
|
btn.add_theme_stylebox_override("pressed", sn)
|
|
else:
|
|
btn.add_theme_color_override("font_color", Color(0.82, 0.84, 0.9))
|
|
btn.add_theme_color_override("font_hover_color", Color(1.0, 1.0, 1.0))
|
|
btn.add_theme_color_override("font_pressed_color", Color(1.0, 1.0, 1.0))
|
|
|
|
var sh := StyleBoxFlat.new()
|
|
sh.bg_color = Color(1.0, 1.0, 1.0, 0.06)
|
|
sh.content_margin_left = 20.0
|
|
sh.border_width_left = 3
|
|
sh.border_color = Color(1.0, 1.0, 1.0, 0.9)
|
|
btn.add_theme_stylebox_override("hover", sh)
|
|
btn.add_theme_stylebox_override("pressed", sh)
|
|
|
|
return btn
|
|
|
|
|
|
func _add_profile_badge() -> void:
|
|
_name_lbl = Label.new()
|
|
_name_lbl.anchor_left = 1.0
|
|
_name_lbl.anchor_top = 0.0
|
|
_name_lbl.anchor_right = 1.0
|
|
_name_lbl.anchor_bottom = 0.0
|
|
_name_lbl.offset_left = -340.0
|
|
_name_lbl.offset_top = 28.0
|
|
_name_lbl.offset_right = -24.0
|
|
_name_lbl.offset_bottom = 50.0
|
|
_name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
_name_lbl.add_theme_font_size_override("font_size", 17)
|
|
_name_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
|
add_child(_name_lbl)
|
|
|
|
_rank_lbl = Label.new()
|
|
_rank_lbl.anchor_left = 1.0
|
|
_rank_lbl.anchor_top = 0.0
|
|
_rank_lbl.anchor_right = 1.0
|
|
_rank_lbl.anchor_bottom = 0.0
|
|
_rank_lbl.offset_left = -340.0
|
|
_rank_lbl.offset_top = 50.0
|
|
_rank_lbl.offset_right = -24.0
|
|
_rank_lbl.offset_bottom = 68.0
|
|
_rank_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
_rank_lbl.add_theme_font_size_override("font_size", 12)
|
|
add_child(_rank_lbl)
|
|
|
|
|
|
func _refresh_profile_badge() -> void:
|
|
if GameConfig.player_name.strip_edges().is_empty():
|
|
_name_lbl.text = "GUEST"
|
|
_name_lbl.add_theme_color_override("font_color", Color(0.7, 0.72, 0.78))
|
|
_rank_lbl.text = "SET A CALLSIGN IN PROFILE"
|
|
_rank_lbl.add_theme_color_override("font_color", Color(0.85, 0.55, 0.3))
|
|
else:
|
|
_name_lbl.text = GameConfig.player_name.to_upper()
|
|
_name_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
|
var rank: Dictionary = RANK_DATA[CURRENT_RANK]
|
|
_rank_lbl.text = "%s • LEVEL %d" % [rank.label, CURRENT_LEVEL]
|
|
_rank_lbl.add_theme_color_override("font_color", rank.color)
|
|
|
|
|
|
func _add_status_label() -> void:
|
|
_status_lbl = Label.new()
|
|
_status_lbl.text = ""
|
|
_status_lbl.anchor_left = 0.0
|
|
_status_lbl.anchor_top = 1.0
|
|
_status_lbl.anchor_right = 0.0
|
|
_status_lbl.anchor_bottom = 1.0
|
|
_status_lbl.offset_left = NAV_LEFT
|
|
_status_lbl.offset_right = NAV_LEFT + 460.0
|
|
_status_lbl.offset_top = -64.0
|
|
_status_lbl.offset_bottom = -36.0
|
|
_status_lbl.add_theme_font_size_override("font_size", 13)
|
|
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
|
add_child(_status_lbl)
|
|
|
|
|
|
func _add_version_label() -> void:
|
|
var lbl := Label.new()
|
|
lbl.text = "v0.1-dev"
|
|
lbl.anchor_left = 0.0
|
|
lbl.anchor_top = 1.0
|
|
lbl.anchor_right = 0.0
|
|
lbl.anchor_bottom = 1.0
|
|
lbl.offset_left = NAV_LEFT
|
|
lbl.offset_top = -30.0
|
|
lbl.offset_right = NAV_LEFT + 100.0
|
|
lbl.offset_bottom = -10.0
|
|
lbl.add_theme_font_size_override("font_size", 11)
|
|
lbl.add_theme_color_override("font_color", Color(0.4, 0.42, 0.48))
|
|
add_child(lbl)
|
|
|
|
|
|
func _add_profile_overlay() -> void:
|
|
_profile_overlay = Control.new()
|
|
_profile_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
_profile_overlay.visible = false
|
|
add_child(_profile_overlay)
|
|
|
|
var dim := ColorRect.new()
|
|
dim.color = Color(0.0, 0.0, 0.0, 0.6)
|
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
dim.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
_profile_overlay.add_child(dim)
|
|
|
|
var panel := Panel.new()
|
|
panel.set_anchors_preset(Control.PRESET_CENTER)
|
|
panel.offset_left = -200
|
|
panel.offset_top = -120
|
|
panel.offset_right = 200
|
|
panel.offset_bottom = 120
|
|
var ps := StyleBoxFlat.new()
|
|
ps.bg_color = Color(0.05, 0.05, 0.07, 0.97)
|
|
ps.set_corner_radius_all(6)
|
|
ps.set_border_width_all(1)
|
|
ps.border_color = Color(1.0, 1.0, 1.0, 0.25)
|
|
panel.add_theme_stylebox_override("panel", ps)
|
|
_profile_overlay.add_child(panel)
|
|
|
|
var vb := VBoxContainer.new()
|
|
vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
vb.offset_left = 26
|
|
vb.offset_top = 22
|
|
vb.offset_right = -26
|
|
vb.offset_bottom = -22
|
|
vb.add_theme_constant_override("separation", 10)
|
|
panel.add_child(vb)
|
|
|
|
var title := Label.new()
|
|
title.text = "PROFILE"
|
|
title.add_theme_font_size_override("font_size", 20)
|
|
title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
|
vb.add_child(title)
|
|
|
|
vb.add_child(HSeparator.new())
|
|
|
|
_profile_hint_lbl = Label.new()
|
|
_profile_hint_lbl.text = ""
|
|
_profile_hint_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD
|
|
_profile_hint_lbl.add_theme_font_size_override("font_size", 12)
|
|
_profile_hint_lbl.add_theme_color_override("font_color", Color(0.85, 0.55, 0.3))
|
|
_profile_hint_lbl.visible = false
|
|
vb.add_child(_profile_hint_lbl)
|
|
|
|
var lbl := Label.new()
|
|
lbl.text = "CALLSIGN"
|
|
lbl.add_theme_font_size_override("font_size", 12)
|
|
lbl.add_theme_color_override("font_color", Color(0.65, 0.68, 0.75))
|
|
vb.add_child(lbl)
|
|
|
|
_profile_input = LineEdit.new()
|
|
_profile_input.placeholder_text = "Enter your callsign..."
|
|
_profile_input.custom_minimum_size = Vector2(0, 40)
|
|
_profile_input.add_theme_font_size_override("font_size", 15)
|
|
_profile_input.text_submitted.connect(func(_t): _on_profile_save())
|
|
vb.add_child(_profile_input)
|
|
|
|
var gap := Control.new()
|
|
gap.custom_minimum_size = Vector2(0, 4)
|
|
gap.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
vb.add_child(gap)
|
|
|
|
var btn_row := HBoxContainer.new()
|
|
btn_row.add_theme_constant_override("separation", 10)
|
|
vb.add_child(btn_row)
|
|
|
|
var cancel_btn := Button.new()
|
|
cancel_btn.text = "CANCEL"
|
|
cancel_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
cancel_btn.custom_minimum_size = Vector2(0, 42)
|
|
cancel_btn.pressed.connect(_on_profile_cancel)
|
|
btn_row.add_child(cancel_btn)
|
|
|
|
var save_btn := Button.new()
|
|
save_btn.text = "SAVE"
|
|
save_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
save_btn.custom_minimum_size = Vector2(0, 42)
|
|
save_btn.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
|
var sn := StyleBoxFlat.new()
|
|
sn.bg_color = Color(1.0, 1.0, 1.0, 0.12)
|
|
sn.set_corner_radius_all(4)
|
|
var sh := StyleBoxFlat.new()
|
|
sh.bg_color = Color(1.0, 1.0, 1.0, 0.22)
|
|
sh.set_corner_radius_all(4)
|
|
save_btn.add_theme_stylebox_override("normal", sn)
|
|
save_btn.add_theme_stylebox_override("hover", sh)
|
|
save_btn.pressed.connect(_on_profile_save)
|
|
btn_row.add_child(save_btn)
|
|
|
|
|
|
func _open_profile(pending_quick_play: bool) -> void:
|
|
_pending_quick_play = pending_quick_play
|
|
_profile_input.text = GameConfig.player_name
|
|
_profile_hint_lbl.visible = pending_quick_play
|
|
_profile_hint_lbl.text = "Enter a callsign to play."
|
|
_profile_overlay.visible = true
|
|
_profile_input.grab_focus()
|
|
|
|
|
|
func _on_profile_save() -> void:
|
|
var new_name := _profile_input.text.strip_edges()
|
|
if new_name.is_empty():
|
|
return
|
|
GameConfig.player_name = new_name
|
|
_refresh_profile_badge()
|
|
_profile_overlay.visible = false
|
|
if _pending_quick_play:
|
|
_pending_quick_play = false
|
|
_start_quick_play()
|
|
|
|
|
|
func _on_profile_cancel() -> void:
|
|
_pending_quick_play = false
|
|
_profile_overlay.visible = false
|
|
|
|
|
|
func _on_quick_play() -> void:
|
|
if _connecting:
|
|
return
|
|
if GameConfig.player_name.strip_edges().is_empty():
|
|
_open_profile(true)
|
|
return
|
|
_start_quick_play()
|
|
|
|
|
|
func _start_quick_play() -> void:
|
|
_connecting = true
|
|
_status_lbl.add_theme_color_override("font_color", Color(0.6, 0.72, 0.86))
|
|
_status_lbl.text = "Searching for a match..."
|
|
MatchmakingClient.start_matchmaking(GameConfig.player_name, "casual")
|
|
|
|
|
|
func _on_server_select() -> void:
|
|
get_tree().change_scene_to_file("res://menu/server_select.tscn")
|
|
|
|
|
|
func _on_options() -> void:
|
|
_status_lbl.add_theme_color_override("font_color", Color(0.5, 0.54, 0.62))
|
|
_status_lbl.text = "Options — coming soon."
|
|
|
|
|
|
func _on_profile() -> void:
|
|
_open_profile(false)
|
|
|
|
|
|
func _on_quit() -> void:
|
|
get_tree().quit()
|
|
|
|
|
|
func _on_match_found(server_ip: String, server_port: int) -> void:
|
|
_status_lbl.text = "Match found — connecting..."
|
|
NetworkManager.join_server(server_ip, server_port)
|
|
|
|
|
|
func _on_connected() -> void:
|
|
get_tree().change_scene_to_file("res://world/world.tscn")
|
|
|
|
|
|
func _on_connect_failed() -> void:
|
|
_connecting = false
|
|
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
|
_status_lbl.text = "Connection failed — is the game server reachable?"
|
|
|
|
|
|
func _on_search_failed(reason: String) -> void:
|
|
_connecting = false
|
|
_status_lbl.add_theme_color_override("font_color", Color(0.85, 0.4, 0.4))
|
|
_status_lbl.text = "Matchmaking failed: %s" % reason
|