extends Control var _ui_vbox: VBoxContainer var _level_selector_panel: VBoxContainer var _multiplayer_panel: VBoxContainer var _direct_connect_panel: VBoxContainer var _host_lobby_panel: Control var _host_lobby_players_list: ItemList func _ready() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) # Comic/cel theme for every Control in the game (root window inherits down) UITheme.apply_global(get_tree()) # Menu music (scene-owned: stops automatically when a level loads). # OGG loops natively over the full track — no manual loop points. var music_path := "res://assets/sounds/music_menu.ogg" if ResourceLoader.exists(music_path): var music := AudioStreamPlayer.new() music.stream = load(music_path) if music.stream is AudioStreamOggVorbis: music.stream.loop = true music.bus = "Music" music.volume_db = -8.0 music.autoplay = true add_child(music) var nm = get_node_or_null("/root/NetworkManager") if nm: nm.connection_succeeded.connect(_on_connection_succeeded) # ========================================== # BACKGROUND: 3D DIORAMA (Sliding Player) # ========================================== var right_panel = SubViewportContainer.new() right_panel.set_anchors_preset(Control.PRESET_FULL_RECT) right_panel.stretch = true right_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(right_panel) var viewport = SubViewport.new() viewport.own_world_3d = true viewport.msaa_3d = Viewport.MSAA_4X right_panel.add_child(viewport) _build_3d_diorama(viewport) # ========================================== # FOREGROUND: UI (Title and Buttons) # ========================================== _ui_vbox = VBoxContainer.new() _ui_vbox.add_theme_constant_override("separation", 30) _ui_vbox.set_anchor_and_offset(SIDE_LEFT, 0.0, 50) _ui_vbox.set_anchor_and_offset(SIDE_TOP, 0.0, 50) _ui_vbox.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600) _ui_vbox.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50) add_child(_ui_vbox) # Title — big comic wordmark with a slight kinetic tilt var title = Label.new() title.text = "PAPAYA SHOOTER" title.add_theme_font_size_override("font_size", 92) title.add_theme_color_override("font_color", UITheme.PAPAYA) title.add_theme_color_override("font_outline_color", UITheme.INK) title.add_theme_constant_override("outline_size", 16) title.rotation_degrees = -2.5 _ui_vbox.add_child(title) # Spacer var spacer = Control.new() spacer.custom_minimum_size = Vector2(0, 40) _ui_vbox.add_child(spacer) # Buttons _add_button(_ui_vbox, "Singleplayer", _on_singleplayer_pressed) _add_button(_ui_vbox, "Multiplayer", _on_multiplayer_pressed) _add_button(_ui_vbox, "Loadouts", _on_loadouts_pressed) _add_button(_ui_vbox, "Settings", _on_settings_pressed) _add_button(_ui_vbox, "Exit Game", _on_exit_pressed) # Skin selector (skins come from SkinManager: built-ins + skins.json) _build_skin_selector(_ui_vbox) # ========================================== # LEVEL SELECTOR PANEL # ========================================== _level_selector_panel = VBoxContainer.new() _level_selector_panel.add_theme_constant_override("separation", 20) _level_selector_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50) _level_selector_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50) _level_selector_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 800) _level_selector_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50) _level_selector_panel.hide() add_child(_level_selector_panel) var ls_title = Label.new() ls_title.text = "Select Level" ls_title.add_theme_font_size_override("font_size", 64) ls_title.add_theme_color_override("font_color", Color(1, 1, 1)) ls_title.add_theme_color_override("font_outline_color", Color(0, 0, 0)) ls_title.add_theme_constant_override("outline_size", 8) _level_selector_panel.add_child(ls_title) # Cards Container var cards_hbox = HBoxContainer.new() cards_hbox.add_theme_constant_override("separation", 30) cards_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL _level_selector_panel.add_child(cards_hbox) _load_maps_modularly(cards_hbox, false, Vector2(300, 400)) var ls_spacer = Control.new() ls_spacer.custom_minimum_size = Vector2(0, 20) _level_selector_panel.add_child(ls_spacer) _add_button(_level_selector_panel, "Back", _on_back_pressed) # ========================================== # MULTIPLAYER PANEL # ========================================== _multiplayer_panel = VBoxContainer.new() _multiplayer_panel.add_theme_constant_override("separation", 20) _multiplayer_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50) _multiplayer_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50) _multiplayer_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600) _multiplayer_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50) _multiplayer_panel.hide() add_child(_multiplayer_panel) var mp_title = Label.new() mp_title.text = "Multiplayer" mp_title.add_theme_font_size_override("font_size", 64) mp_title.add_theme_color_override("font_color", Color(1, 1, 1)) mp_title.add_theme_color_override("font_outline_color", Color(0, 0, 0)) mp_title.add_theme_constant_override("outline_size", 8) _multiplayer_panel.add_child(mp_title) var mp_spacer = Control.new() mp_spacer.custom_minimum_size = Vector2(0, 20) _multiplayer_panel.add_child(mp_spacer) _add_button(_multiplayer_panel, "Host Game", _on_host_pressed) _add_button(_multiplayer_panel, "Direct Connect", _on_direct_connect_pressed) _add_button(_multiplayer_panel, "Back", _on_multiplayer_back_pressed) # ========================================== # DIRECT CONNECT PANEL # ========================================== _direct_connect_panel = VBoxContainer.new() _direct_connect_panel.add_theme_constant_override("separation", 20) _direct_connect_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50) _direct_connect_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50) _direct_connect_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600) _direct_connect_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50) _direct_connect_panel.hide() add_child(_direct_connect_panel) var dc_title = Label.new() dc_title.text = "Direct Connect" dc_title.add_theme_font_size_override("font_size", 64) dc_title.add_theme_color_override("font_color", Color(1, 1, 1)) dc_title.add_theme_color_override("font_outline_color", Color(0, 0, 0)) dc_title.add_theme_constant_override("outline_size", 8) _direct_connect_panel.add_child(dc_title) var dc_ip_label = Label.new() dc_ip_label.text = "IP Address:" dc_ip_label.add_theme_font_size_override("font_size", 24) _direct_connect_panel.add_child(dc_ip_label) var dc_ip_input = LineEdit.new() dc_ip_input.text = "127.0.0.1" dc_ip_input.name = "IPInput" dc_ip_input.add_theme_font_size_override("font_size", 24) dc_ip_input.custom_minimum_size = Vector2(300, 50) _direct_connect_panel.add_child(dc_ip_input) var dc_port_label = Label.new() dc_port_label.text = "Port:" dc_port_label.add_theme_font_size_override("font_size", 24) _direct_connect_panel.add_child(dc_port_label) var dc_port_input = LineEdit.new() dc_port_input.text = "31415" # Default Port dc_port_input.name = "PortInput" dc_port_input.add_theme_font_size_override("font_size", 24) dc_port_input.custom_minimum_size = Vector2(300, 50) _direct_connect_panel.add_child(dc_port_input) var dc_spacer = Control.new() dc_spacer.custom_minimum_size = Vector2(0, 20) _direct_connect_panel.add_child(dc_spacer) _add_button(_direct_connect_panel, "Connect", _on_connect_confirm_pressed) _add_button(_direct_connect_panel, "Back", _on_dc_back_pressed) # ========================================== # HOST LOBBY PANEL # ========================================== _host_lobby_panel = Control.new() _host_lobby_panel.set_anchors_preset(Control.PRESET_FULL_RECT) _host_lobby_panel.hide() add_child(_host_lobby_panel) var hl_hbox = HBoxContainer.new() hl_hbox.set_anchors_preset(Control.PRESET_FULL_RECT) hl_hbox.add_theme_constant_override("separation", 50) _host_lobby_panel.add_child(hl_hbox) # Left Side (Level Selector) var hl_left = VBoxContainer.new() hl_left.size_flags_horizontal = Control.SIZE_EXPAND_FILL hl_left.set_anchor_and_offset(SIDE_LEFT, 0.0, 50) hl_left.set_anchor_and_offset(SIDE_TOP, 0.0, 50) hl_left.add_theme_constant_override("separation", 20) var hl_margin = MarginContainer.new() hl_margin.add_theme_constant_override("margin_left", 50) hl_margin.add_theme_constant_override("margin_top", 50) hl_margin.add_theme_constant_override("margin_bottom", 50) hl_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL hl_hbox.add_child(hl_margin) hl_margin.add_child(hl_left) var hl_title = Label.new() hl_title.text = "Host Lobby - Select Level" hl_title.add_theme_font_size_override("font_size", 64) hl_title.add_theme_color_override("font_color", Color(1, 1, 1)) hl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0)) hl_title.add_theme_constant_override("outline_size", 8) hl_left.add_child(hl_title) var hl_cards = HBoxContainer.new() hl_cards.add_theme_constant_override("separation", 20) hl_left.add_child(hl_cards) _load_maps_modularly(hl_cards, true, Vector2(200, 250)) var mode_label = Label.new() mode_label.text = "Gamemode" mode_label.add_theme_font_size_override("font_size", 32) hl_left.add_child(mode_label) var _gamemode_opt = OptionButton.new() _gamemode_opt.add_theme_font_size_override("font_size", 24) _gamemode_opt.add_item("Deathmatch", 0) _gamemode_opt.add_to_group("host_only_ui") _gamemode_opt.item_selected.connect(func(index: int): if get_node_or_null("/root/NetworkManager"): get_node("/root/NetworkManager").current_gamemode = _gamemode_opt.get_item_text(index) ) hl_left.add_child(_gamemode_opt) var hl_spacer = Control.new() hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL hl_left.add_child(hl_spacer) _add_button(hl_left, "Disband & Back", _on_lobby_back_pressed) # Right Side (Player List) var hl_right = VBoxContainer.new() hl_right.custom_minimum_size = Vector2(400, 0) var hl_right_margin = MarginContainer.new() hl_right_margin.add_theme_constant_override("margin_top", 50) hl_right_margin.add_theme_constant_override("margin_right", 50) hl_right_margin.add_theme_constant_override("margin_bottom", 50) hl_hbox.add_child(hl_right_margin) hl_right_margin.add_child(hl_right) var pl_title = Label.new() pl_title.text = "Players Connected" pl_title.add_theme_font_size_override("font_size", 48) pl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0)) pl_title.add_theme_constant_override("outline_size", 8) hl_right.add_child(pl_title) var port_label = Label.new() if get_node_or_null("/root/NetworkManager"): port_label.text = "Port: " + str(get_node("/root/NetworkManager").DEFAULT_PORT) port_label.add_theme_font_size_override("font_size", 24) port_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) hl_right.add_child(port_label) _host_lobby_players_list = ItemList.new() _host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL _host_lobby_players_list.add_theme_font_size_override("font_size", 24) hl_right.add_child(_host_lobby_players_list) # Hover/click sounds on every menu button UITheme.wire_sounds(self) func _load_maps_modularly(target_hbox: Container, is_multiplayer: bool, card_size: Vector2) -> void: var maps_dir = "res://scenes/maps/" var dir = DirAccess.open(maps_dir) if not dir: return dir.list_dir_begin() var folder_name = dir.get_next() while folder_name != "": if dir.current_is_dir() and not folder_name.begins_with("."): var cfg_path = maps_dir + folder_name + "/map_meta.cfg" if FileAccess.file_exists(cfg_path): var cfg = ConfigFile.new() if cfg.load(cfg_path) == OK: var map_name = cfg.get_value("map", "name", "Unknown Map") var scene_path = cfg.get_value("map", "scene_path", "") var c1 = cfg.get_value("map", "color1", Color(0.5, 0.5, 0.5)) var c2 = cfg.get_value("map", "color2", Color(0.2, 0.2, 0.2)) if scene_path != "": var grad = Gradient.new() # A new Gradient has 2 points at 0.0 and 1.0 by default. Set their colors directly. grad.set_color(0, c1) grad.set_color(1, c2) _build_level_card(target_hbox, map_name, scene_path, grad, is_multiplayer, card_size) folder_name = dir.get_next() func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false, card_size: Vector2 = Vector2(300, 400)) -> void: var btn = TextureButton.new() btn.custom_minimum_size = card_size var tex = GradientTexture2D.new() tex.gradient = gradient tex.width = int(card_size.x) tex.height = int(card_size.y) tex.fill_to = Vector2(1, 1) btn.texture_normal = tex var tex_hover = GradientTexture2D.new() tex_hover.gradient = gradient tex_hover.width = int(card_size.x) tex_hover.height = int(card_size.y) tex_hover.fill_to = Vector2(1, 1) tex_hover.use_hdr = true # Slight bright effect on hover btn.texture_hover = tex_hover btn.modulate = Color(1, 1, 1) # VBox to hold the label var vbox = VBoxContainer.new() vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.alignment = BoxContainer.ALIGNMENT_END btn.add_child(vbox) # Dimming overlay at the bottom for text readability var overlay = ColorRect.new() overlay.color = Color(0, 0, 0, 0.6) overlay.custom_minimum_size = Vector2(0, 60) vbox.add_child(overlay) var lbl = Label.new() lbl.text = title lbl.add_theme_font_size_override("font_size", 32) lbl.add_theme_color_override("font_color", Color.WHITE) lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.set_anchors_preset(Control.PRESET_FULL_RECT) overlay.add_child(lbl) if is_multiplayer: btn.add_to_group("host_only_ui") btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path)) else: btn.pressed.connect(func(): var nm = get_node_or_null("/root/NetworkManager") if nm: nm.start_singleplayer_match("Deathmatch") get_tree().change_scene_to_file(scene_path) ) parent.add_child(btn) func _add_button(parent: Container, text: String, callback: Callable) -> void: var btn = Button.new() btn.text = text btn.add_theme_font_size_override("font_size", 32) btn.custom_minimum_size = Vector2(300, 60) btn.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN btn.pressed.connect(callback) parent.add_child(btn) func _build_skin_selector(parent: Container) -> void: var skin_mgr = get_node_or_null("/root/SkinManager") if not skin_mgr: return var row = HBoxContainer.new() row.add_theme_constant_override("separation", 12) parent.add_child(row) var lbl = Label.new() lbl.text = "Skin" lbl.add_theme_font_size_override("font_size", 24) row.add_child(lbl) var opt = OptionButton.new() opt.custom_minimum_size = Vector2(240, 44) var ids: Array = skin_mgr.get_skin_ids() ids.sort() for i in range(ids.size()): var skin = skin_mgr.get_skin(ids[i]) opt.add_item(skin.skin_name, i) opt.set_item_metadata(i, ids[i]) if ids[i] == skin_mgr.active_skin_id: opt.select(i) opt.item_selected.connect(func(idx: int): skin_mgr.set_active_skin(opt.get_item_metadata(idx)) ) row.add_child(opt) func _on_back_pressed() -> void: _level_selector_panel.hide() _ui_vbox.show() func _on_singleplayer_pressed() -> void: _ui_vbox.hide() _level_selector_panel.show() func _on_multiplayer_pressed() -> void: _ui_vbox.hide() _multiplayer_panel.show() func _on_multiplayer_back_pressed() -> void: _multiplayer_panel.hide() _ui_vbox.show() func _on_host_pressed() -> void: var nm = get_node("/root/NetworkManager") if nm.host_game() == OK: _on_connection_succeeded() func _on_direct_connect_pressed() -> void: _multiplayer_panel.hide() _direct_connect_panel.show() func _on_dc_back_pressed() -> void: _direct_connect_panel.hide() _multiplayer_panel.show() func _on_connect_confirm_pressed() -> void: var ip = _direct_connect_panel.get_node("IPInput").text var port = _direct_connect_panel.get_node("PortInput").text.to_int() get_node("/root/NetworkManager").join_game(ip, port) func _on_connection_succeeded() -> void: _direct_connect_panel.hide() _multiplayer_panel.hide() _host_lobby_panel.show() var nm = get_node("/root/NetworkManager") var is_host = nm.multiplayer.is_server() for node in get_tree().get_nodes_in_group("host_only_ui"): if node is BaseButton: node.disabled = not is_host if not nm.player_connected.is_connected(_on_network_player_connected): nm.player_connected.connect(_on_network_player_connected) nm.player_disconnected.connect(_on_network_player_disconnected) if not nm.stats_updated.is_connected(_update_lobby_players): nm.stats_updated.connect(_update_lobby_players) _update_lobby_players() func _on_lobby_back_pressed() -> void: _host_lobby_panel.hide() _multiplayer_panel.show() var nm = get_node("/root/NetworkManager") nm.disconnect_game() if nm.player_connected.is_connected(_on_network_player_connected): nm.player_connected.disconnect(_on_network_player_connected) nm.player_disconnected.disconnect(_on_network_player_disconnected) if nm.stats_updated.is_connected(_update_lobby_players): nm.stats_updated.disconnect(_update_lobby_players) func _on_network_player_connected(_id: int) -> void: _update_lobby_players() func _on_network_player_disconnected(_id: int) -> void: _update_lobby_players() func _update_lobby_players() -> void: if not _host_lobby_players_list: return _host_lobby_players_list.clear() var nm = get_node("/root/NetworkManager") for pid in nm.player_stats.keys(): var p_name = nm.player_stats[pid].get("username", "Unknown") var p_ping = nm.player_stats[pid].get("ping", 0) if pid == 1: _host_lobby_players_list.add_item("%s (Host) - %sms" % [p_name, p_ping]) else: _host_lobby_players_list.add_item("%s - %sms" % [p_name, p_ping]) func _on_loadouts_pressed() -> void: if has_node("/root/PauseMenu"): var pm = get_node("/root/PauseMenu") pm.visible = true pm._show_loadouts() func _on_settings_pressed() -> void: if has_node("/root/PauseMenu"): var pm = get_node("/root/PauseMenu") pm.visible = true pm._show_settings() func _on_exit_pressed() -> void: get_tree().quit() # ========================================== # 3D DIORAMA BUILDER # ========================================== func _build_3d_diorama(viewport: SubViewport) -> void: var world = Node3D.new() viewport.add_child(world) # Lighting and Env — same stylized sunset sky as in-game for continuity var env = WorldEnvironment.new() env.environment = LevelEnvironment.make_environment("sunset") world.add_child(env) var sun = DirectionalLight3D.new() sun.rotation_degrees = Vector3(-45, 45, 0) sun.light_color = Color(1.0, 0.9, 0.8) sun.light_energy = 1.5 sun.shadow_enabled = true world.add_child(sun) var fill = DirectionalLight3D.new() fill.rotation_degrees = Vector3(-30, -135, 0) fill.light_color = Color(0.5, 0.6, 1.0) fill.light_energy = 0.5 world.add_child(fill) # Floor — toon world-grid like the levels var floor_mesh = MeshInstance3D.new() floor_mesh.mesh = PlaneMesh.new() floor_mesh.mesh.size = Vector2(20, 20) floor_mesh.mesh.surface_set_material(0, LevelMaterials.tinted(Color(0.32, 0.3, 0.38))) world.add_child(floor_mesh) # Player Model (Sliding Pose) var player_model = Node3D.new() player_model.position = Vector3(0, 0.5, 0) # Lowered for slide world.add_child(player_model) var body_mat = StandardMaterial3D.new() body_mat.albedo_color = Color(0.3, 0.6, 0.9) # Toonified below via apply_toon_recursive once the model is assembled # Torso (Capsule tilted back) var torso = MeshInstance3D.new() var t_mesh = CapsuleMesh.new() t_mesh.radius = 0.4 t_mesh.height = 1.2 t_mesh.material = body_mat torso.mesh = t_mesh torso.rotation_degrees.x = -60 # Tilted back torso.position = Vector3(0, 0.4, 0) player_model.add_child(torso) # Legs (Sticking forward) var legs = MeshInstance3D.new() var l_mesh = BoxMesh.new() l_mesh.size = Vector3(0.6, 0.4, 1.2) l_mesh.material = body_mat legs.mesh = l_mesh legs.position = Vector3(0, -0.2, 0.6) player_model.add_child(legs) # Arm holding shotgun var arm = Node3D.new() arm.position = Vector3(0.5, 0.6, 0.2) player_model.add_child(arm) var arm_mesh = MeshInstance3D.new() var a_mesh = BoxMesh.new() a_mesh.size = Vector3(0.2, 0.2, 0.8) a_mesh.material = body_mat arm_mesh.mesh = a_mesh arm_mesh.position = Vector3(0, 0, 0.3) arm.add_child(arm_mesh) # Shotgun var shotgun_script = load("res://weapons/double_barrel_shotgun.gd") if shotgun_script: var shotgun = shotgun_script.new() arm_mesh.add_child(shotgun) shotgun.position = Vector3(0, -0.1, 0.4) shotgun.rotation_degrees = Vector3(0, 0, 0) shotgun.set_process(false) shotgun.set_physics_process(false) if shotgun.has_method("set_process_input"): shotgun.set_process_input(false) # Cel-shade the diorama (no hull outlines — the FBX shotgun tears them) LevelMaterials.apply_toon_recursive(player_model, 0.0) # Camera var camera = Camera3D.new() world.add_child(camera) camera.position = Vector3(3, 1.5, 4) camera.look_at(Vector3(0, 0.5, 0))