Onboarding + loop flow for a hands-off demo; GoalTarget 4->2

Lap-1 Fabricator affordable (StartingOre 50->90); mine prompt shows IN the room;
Return no longer false-completes on death (WasOnExpedition latch + soft timeout,
never the 1-tick Returning edge); HUD keeps room/siege/ammo cues during combat
steps; Welcome needs an explicit confirm; READY panel shows FINAL DEFENSE when
goal-full; dev Force-Each-Launch hidden from public Settings. GoalProgress.Target
4->2 (~10-20 min demo). Onboarding tests updated to the new contracts (456/456).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:21:42 -07:00
parent 43d7cca2f8
commit ab657fa578
6 changed files with 67 additions and 21 deletions
@@ -42,6 +42,8 @@ namespace ProjectM.Client
float _moveAccum;
float3 _lastPos;
bool _sawSiege;
bool _wasOnExpedition; // D3: latched true once the player is seen on expedition during the Return step
protected override void OnStartRunning()
{
@@ -64,6 +66,8 @@ namespace ProjectM.Client
var root = _doc.rootVisualElement;
if (root == null) return;
if (!_built) { BuildTree(root); _built = true; }
OnboardingState.SuppressLocationLine = false; // D4: default off each frame; a shown step sets it per-step below
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta — correct in a presentation system
@@ -106,7 +110,7 @@ namespace ProjectM.Client
// ---- per-step entry init (baselines) ----
if (!_stepInit)
{
_stepElapsed = 0f; _moveAccum = 0f; _sawSiege = false;
_stepElapsed = 0f; _moveAccum = 0f; _sawSiege = false; _wasOnExpedition = false;
_lastPos = playerPos;
_stepInit = true;
}
@@ -118,6 +122,8 @@ namespace ProjectM.Client
_stepElapsed += dt;
if (_step == OnboardingStepMath.Move) _moveAccum += math.distance(playerPos, _lastPos);
if (_step == OnboardingStepMath.Defend && phase == CyclePhase.Siege) _sawSiege = true;
if (_step == OnboardingStepMath.Return && onExp) _wasOnExpedition = true; // D3: latch "was on expedition" so a start-at-base Return doesn't instantly satisfy
var snap = new OnboardingStepMath.Snapshot
{
@@ -130,12 +136,13 @@ namespace ProjectM.Client
OnExpedition = onExp,
ObjectiveState = objState,
SawSiege = _sawSiege,
WasOnExpedition = _wasOnExpedition,
Phase = phase,
};
// The two pure-message beats can be dismissed with any input EXCEPT Esc (Esc opens Pause; see
// AnyInputPressed) so following the "Esc → Pause → How to Play" hint doesn't self-skip the framing.
bool skip = (_step == OnboardingStepMath.Welcome || _step == OnboardingStepMath.Done) && AnyInputPressed();
bool skip = (_step == OnboardingStepMath.Welcome && ConfirmPressed()) || (_step == OnboardingStepMath.Done && AnyInputPressed()); // D5: Welcome dismisses ONLY on an explicit confirm (not WASD) so the win-condition strip is actually read
if (skip || OnboardingStepMath.IsSatisfied(_step, snap))
{
_mask |= (1 << _step);
@@ -154,6 +161,7 @@ namespace ProjectM.Client
// ---- show the current step ----
OnboardingState.Active = true;
OnboardingState.SuppressLocationLine = OnboardingStepMath.IsEarlyStep(_step); // D4: blank the HUD line only for the early base-framing steps
root.style.display = DisplayStyle.Flex;
bool gamepad = AimPresentation.Scheme == InputSchemeId.Gamepad;
_prompt.text = OnboardingStepMath.Prompt(_step, gamepad);
@@ -194,7 +202,20 @@ namespace ProjectM.Client
return false;
}
// ---- world-space pointer ----
// D5: an EXPLICIT confirm only (Space/Enter/click/South) — movement keys must NOT skip the Welcome strip.
static bool ConfirmPressed()
{
var kb = UnityEngine.InputSystem.Keyboard.current;
if (kb != null && (kb.spaceKey.wasPressedThisFrame || kb.enterKey.wasPressedThisFrame)) return true;
var ms = UnityEngine.InputSystem.Mouse.current;
if (ms != null && ms.leftButton.wasPressedThisFrame) return true;
var gp = UnityEngine.InputSystem.Gamepad.current;
if (gp != null && (gp.buttonSouth.wasPressedThisFrame || gp.startButton.wasPressedThisFrame)) return true;
return false;
}
// ---- world-space pointer ----
bool ResolveTarget(byte kind, float3 playerPos, out float3 target)
{