First-run onboarding: contextual coach-marks + How-to-Play card + dev replay toggle
Teaches the deep, interlocking loop — especially the inverted win condition (you win by CLEARING EXPEDITIONS, not by surviving base sieges; DR-042/DR-043). - OnboardingSystem: client-only observe-only PresentationSystemGroup overlay (own UIDocument @ sortingOrder 60), soft-gated 10-beat coach-mark sequence with a world-space ▶ pointer; never mutates sim / never destroys a ghost. - OnboardingStepMath: pure, unit-tested step machine (snapshot + IsSatisfied + scheme-aware prompts + pointer kinds + persisted-mask helpers). - HowToPlayPanel: tabbed reference card (Controls / The Loop / Build / Threats / Win-Lose), reachable from the main menu and the pause overlay. - Per-client client-local state in GameSettings (TutorialHints + OnboardingMask bitmask, additive) — a Join client keeps its own; a host save-wipe never re-teaches. Settings toggle + menu "Replay Tutorial". - Dev "Force Each Launch" toggle (GameSettings.ForceOnboardingEachLaunch): SettingsService.Boot wipes the mask + forces hints on in-memory every launch so the tutorial always replays fresh. - HudSystem suppresses its own location hint while onboarding is active (single prompt voice), via OnboardingState + [UpdateAfter(OnboardingSystem)]. Validated green: 20/20 EditMode; Play smoke confirmed overlay render, clean U+25B6 pointer glyph, no system sort-cycle, and the force-wipe end-to-end. Docs: DR-043 + session log; reusable lesson archived in the build-gotchas note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,18 @@ namespace ProjectM.Client
|
||||
static void Boot()
|
||||
{
|
||||
Load();
|
||||
// DEV convenience ("Force Each Launch", Settings → Onboarding): wipe the persisted completed-step mask at
|
||||
// EVERY boot — each editor Play-enter / each built-player launch runs this hook — so the first-run
|
||||
// coach-marks always replay from the top, and force hints on so they actually show. In-memory only (the
|
||||
// wipe is NOT written back to disk); OnboardingSystem re-persists progress as the player advances, and the
|
||||
// next launch wipes it again. Toggle it off to return to normal once-only first-run behaviour.
|
||||
if (Current.ForceOnboardingEachLaunch != 0)
|
||||
{
|
||||
var s = Current;
|
||||
s.OnboardingMask = 0;
|
||||
s.TutorialHints = 1;
|
||||
Current = s;
|
||||
}
|
||||
Apply(Current);
|
||||
}
|
||||
|
||||
@@ -103,14 +115,21 @@ namespace ProjectM.Client
|
||||
// Additive-only as the schema grows (never throws on an unknown version).
|
||||
static GameSettings Migrate(GameSettings old)
|
||||
{
|
||||
var def = GameSettings.Defaults();
|
||||
if (old.ResWidth > 0) def.ResWidth = old.ResWidth;
|
||||
if (old.ResHeight > 0) def.ResHeight = old.ResHeight;
|
||||
if (old.Master > 0f) def.Master = old.Master;
|
||||
if (old.Music > 0f) def.Music = old.Music;
|
||||
if (old.Sfx > 0f) def.Sfx = old.Sfx;
|
||||
def.Version = GameSettings.CurrentVersion;
|
||||
return def;
|
||||
// Preserve EVERY recognized field from the old save (Load() Clamps the result, fixing any 0/garbage),
|
||||
// and seed ONLY the genuinely-new fields — a v1 file deserializes those to 0. Migrating from a fresh
|
||||
// Defaults() instead would silently reset graphics fields (display mode / quality / v-sync / fps cap /
|
||||
// refresh rate) that v1 already carried — a regression the version bump would otherwise surface.
|
||||
var s = old;
|
||||
if (old.Version < 2)
|
||||
{
|
||||
// v1 had no onboarding fields. An existing settings.json ⇒ a returning player who already played a
|
||||
// pre-onboarding build, so mark every coach-mark step complete (dormant); hints stay on so
|
||||
// "Replay Tutorial" (which clears OnboardingMask) still re-arms.
|
||||
s.TutorialHints = 1;
|
||||
s.OnboardingMask = int.MaxValue;
|
||||
}
|
||||
s.Version = GameSettings.CurrentVersion;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user