From 431a7e2ed9264147155985660c29cee2e03d684a Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Thu, 18 Jun 2026 00:39:38 -0700 Subject: [PATCH] Slice 2: menu class picker (Warrior / Ranger) MainMenuController gains a 2-class picker that sets WorldLauncher.SelectedClass; WorldLauncher seeds a ClassSelection singleton into the client world at session start, which GoInGameClientSystem carries on the spawn RPC. Default Warrior. Completes the Slice 2 loop: pick a class in the menu -> spawn with its kit. Editor-default boot stays Warrior (the menu path drives the choice). 348/348. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Scripts/Client/UI/MainMenuController.cs | 20 +++++++++++++++++++ .../Scripts/Client/UI/WorldLauncher.cs | 13 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Assets/_Project/Scripts/Client/UI/MainMenuController.cs b/Assets/_Project/Scripts/Client/UI/MainMenuController.cs index 2c433a80f..d7db49edc 100644 --- a/Assets/_Project/Scripts/Client/UI/MainMenuController.cs +++ b/Assets/_Project/Scripts/Client/UI/MainMenuController.cs @@ -19,6 +19,7 @@ namespace ProjectM.Client VisualElement _mainPanel; VisualElement _settingsPanel; TextField _ipField; + Label _classLabel; void Awake() { @@ -53,6 +54,17 @@ namespace ProjectM.Client _mainPanel = MenuUi.FullScreenRoot(true); var card = MenuUi.Card("PROJECT M"); card.Add(MenuUi.Caption("Frontier colony — co-op")); + // Slice 2: class picker -> sets WorldLauncher.SelectedClass for the next session (Warrior melee / Ranger ranged). + _classLabel = new Label(ClassName(WorldLauncher.SelectedClass)); + _classLabel.style.unityTextAlign = TextAnchor.MiddleCenter; + _classLabel.style.marginTop = 6; _classLabel.style.marginBottom = 2; + card.Add(_classLabel); + var classRow = new VisualElement(); + classRow.style.flexDirection = FlexDirection.Row; + classRow.style.justifyContent = Justify.Center; + classRow.Add(MenuUi.Button("Warrior", () => SelectClass((byte)CharacterId.Warrior))); + classRow.Add(MenuUi.Button("Ranger", () => SelectClass((byte)CharacterId.Ranger))); + card.Add(classRow); card.Add(MenuUi.Button("Single Player", () => Launch(SessionMode.Single, false))); @@ -79,6 +91,14 @@ namespace ProjectM.Client WorldLauncher.StartSession(mode, ip, loadSave); } + void SelectClass(byte classId) + { + WorldLauncher.SelectedClass = classId; + if (_classLabel != null) _classLabel.text = ClassName(classId); + } + + static string ClassName(byte classId) => classId == (byte)CharacterId.Ranger ? "CLASS: Ranger (ranged)" : "CLASS: Warrior (melee)"; + void ShowSettings() { _mainPanel.style.display = DisplayStyle.None; diff --git a/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs b/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs index 94919ecee..042261add 100644 --- a/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs +++ b/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs @@ -28,6 +28,9 @@ namespace ProjectM.Client public static bool Busy { get; private set; } + /// Slice 2: the class chosen in the menu (a CharacterId byte), seeded into the client world at session start. + public static byte SelectedClass = (byte)CharacterId.Warrior; + public static void StartSession(SessionMode mode, string joinIp, bool loadSave) { if (Busy) return; @@ -51,6 +54,7 @@ namespace ProjectM.Client World server = null; World client = ClientServerBootstrap.CreateClientWorld("ClientWorld"); + SeedClass(client, SelectedClass); // Slice 2: stage the chosen class for GoInGameClientSystem -> spawn if (mode == SessionMode.Join) { @@ -101,6 +105,15 @@ namespace ProjectM.Client }); } + static void SeedClass(World world, byte classId) + { + if (world is not { IsCreated: true }) return; + var em = world.EntityManager; + using var q = em.CreateEntityQuery(ComponentType.ReadWrite()); + Entity e = q.IsEmptyIgnoreFilter ? em.CreateEntity(typeof(ClassSelection)) : q.GetSingletonEntity(); + em.SetComponentData(e, new ClassSelection { ClassId = classId }); + } + static void StagePendingSave(World server) { var data = SaveService.Load();