@energy8platform/platform-core 0.30.10 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -504,7 +504,61 @@ interface AssetBundle {
504
504
  interface AssetManifest {
505
505
  bundles: AssetBundle[];
506
506
  }
507
+ /**
508
+ * A loading overlay the GAME owns, covering the gap BEFORE the engine's own loading screen exists:
509
+ * from the browser's first paint to the first frame the engine paints. It does not replace the
510
+ * loading screen — see `LoadingScreenConfig.externalOverlay`.
511
+ *
512
+ * Described structurally on purpose. The implementation this exists for is Artube's
513
+ * `LoaderViewController` (vendored at `@energy8platform/artube-bridge/loader`), whose markup their
514
+ * Vite plugin injects into `index.html` so the overlay is painted before our bundle is even
515
+ * fetched. Naming that type here would tie every consumer of platform-core to the Artube packages;
516
+ * the shape is satisfied with no adapter, and nothing in `@energy8platform/platform-core` knows
517
+ * Artube exists.
518
+ *
519
+ * The engine's use of it, in order:
520
+ * - `showLoader()` once, at the very start of boot, before anything that can throw;
521
+ * - `updateProgress(0..100)` at boot milestones (Pixi up, SDK handshake done, subsystems up) —
522
+ * real progress through the gap, not the asset loading that follows;
523
+ * - `hideLoader()` the moment the engine's loading screen has painted its first frame — AND on
524
+ * every failure path, so a boot that throws can never leave the overlay on screen forever.
525
+ */
526
+ interface ExternalLoadingOverlay {
527
+ /** Reveal the overlay / its progress affordance. Called once, at the start of boot. */
528
+ showLoader(): void;
529
+ /** Loading progress as a PERCENTAGE, 0–100 (what `ILoaderViewController` expects — the engine's
530
+ * internal 0..1 fraction is converted at the seam). */
531
+ updateProgress(value: number): void;
532
+ /** Dismiss the overlay. Called once; must tolerate being called after a failed boot. */
533
+ hideLoader(): void;
534
+ }
507
535
  interface LoadingScreenConfig {
536
+ /**
537
+ * A game-supplied loading overlay covering ONLY the gap before the engine's own loading screen
538
+ * paints — Artube's branded loader is the case this exists for. It is not a replacement: once the
539
+ * loading screen has painted its first frame, the overlay is dismissed and the rest of the boot
540
+ * (brand, progress bar, tap-to-start, minimum display time) is exactly what it is on every other
541
+ * target. Every other option in this object therefore still applies.
542
+ *
543
+ * The two never stack visibly: the built-in preloader is mounted only at the hand-over, and it
544
+ * covers the external overlay (z-index) from the frame it appears in, so there is no gap, no
545
+ * flash and no bare background at the seam.
546
+ */
547
+ externalOverlay?: ExternalLoadingOverlay;
548
+ /**
549
+ * How long {@link externalOverlay} is guaranteed on screen, in ms, measured from the boot's very
550
+ * first step. Default 1500.
551
+ *
552
+ * The gap this overlay covers is short — a warm boot hands over in a few hundred milliseconds —
553
+ * and a partner's branding that flashes past in under a second has not been shown. The floor also
554
+ * gives a two-phase overlay (Artube's) room to reach its second phase and settle there instead of
555
+ * being cut mid-crossfade. It costs a slow boot nothing: the hand-over happens later than this
556
+ * anyway.
557
+ *
558
+ * Distinct from {@link minDisplayTime}, which is the minimum for the engine's OWN loading screen
559
+ * and is measured from the hand-over. A game that sets both is asking for the sum.
560
+ */
561
+ externalOverlayMinDisplayTime?: number;
508
562
  /**
509
563
  * Which visual identity the CSS preloader renders. Defaults to `'energy8'`;
510
564
  * an unknown value falls back to the default. Ignored when `cssPreloaderHTML`
@@ -544,6 +598,130 @@ declare function setCSSPreloaderProgress(progress: number): void;
544
598
  declare function waitCSSPreloaderTap(): Promise<void>;
545
599
  declare function removeCSSPreloader(_container: HTMLElement): Promise<void>;
546
600
 
601
+ /**
602
+ * A loading overlay the GAME owns, covering the gap the engine cannot: from the
603
+ * browser's first paint until the engine's own loading screen is on screen.
604
+ *
605
+ * The case this exists for is Artube. Their platform ships a branded loader
606
+ * whose markup a Vite plugin injects into `index.html`, so it is painted before
607
+ * the game bundle has even been fetched — earlier than any engine code can
608
+ * possibly run. That window (bundle download → Pixi init → SDK handshake) is
609
+ * otherwise a blank page.
610
+ *
611
+ * ── Where it stops ──────────────────────────────────────────────────────────
612
+ * It stops once BOTH are true: the engine's own loading screen has painted its
613
+ * first frame, and the overlay has had its guaranteed time on screen
614
+ * ({@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}, `loading.externalOverlayMinDisplayTime`).
615
+ * The gap alone is often only a few hundred milliseconds, which is not long
616
+ * enough for a partner's branding to register — let alone for their two-phase
617
+ * screen to reach its second phase.
618
+ *
619
+ * From there the player gets the engine's brand, the engine's progress bar and
620
+ * the engine's tap-to-start — exactly as on every other target. The external
621
+ * overlay covers the gap; it does not replace the loading screen, and it has no
622
+ * say in the tap gate: by then the player is looking at OUR screen.
623
+ *
624
+ * (This inverts an earlier design in which the external overlay REPLACED the
625
+ * CSS preloader for the whole boot. Handing over at the first frame keeps the
626
+ * game's own loading identity on every platform and leaves the built-in
627
+ * preloader's code path untouched — `CSSPreloader.ts` knows nothing about any
628
+ * of this, so non-Artube targets cannot be affected by a change here.)
629
+ *
630
+ * ── Structural, not nominal ─────────────────────────────────────────────────
631
+ * Nothing in `@energy8platform` names Artube's controller. The game passes an
632
+ * instance; we describe the shape (`ExternalLoadingOverlay`). Artube's vendored
633
+ * `LoaderViewController` (`@energy8platform/artube-bridge/loader`) satisfies it
634
+ * with no adapter.
635
+ *
636
+ * ── Module-level state ──────────────────────────────────────────────────────
637
+ * Singleton, like the CSS preloader next door, and for the same reason: the
638
+ * three call sites (boot, hand-over, failure path) are in different files and
639
+ * there is exactly one loading screen per page.
640
+ */
641
+
642
+ /**
643
+ * How long a game-supplied overlay is guaranteed on screen, measured from
644
+ * adoption (which is the first thing the boot does, so in practice from the
645
+ * page's first paint).
646
+ *
647
+ * Without a floor the overlay lives exactly as long as the gap it covers, and
648
+ * that gap is short: a warm local boot handed over at ~500ms and dismissed
649
+ * their screen at ~840ms. A partner's branding that flashes past in under a
650
+ * second has not been shown. 1500ms is the user's number, and it is also the
651
+ * smallest one that fits their two-phase screen whole — a 500ms crossfade into
652
+ * the branded phase plus time to read it — with room for the 300ms dismissal
653
+ * fade on top.
654
+ *
655
+ * Overridable per game: `loading.externalOverlayMinDisplayTime`.
656
+ */
657
+ declare const DEFAULT_EXTERNAL_MIN_DISPLAY_MS = 1500;
658
+ /** Whether a game-supplied overlay is currently on screen and owned by us. */
659
+ declare function hasExternalOverlay(): boolean;
660
+ /**
661
+ * Take ownership of the game's overlay: from here on the engine is responsible
662
+ * for taking it down, including when the boot fails.
663
+ *
664
+ * Called BEFORE anything in the boot that can throw — notably before the
665
+ * container selector is resolved. Until the engine has adopted it, nothing can
666
+ * dismiss it, and Artube's is already on screen from `index.html`.
667
+ *
668
+ * `showLoader()` is Artube's "reveal the progress affordance"; it does not
669
+ * switch their two-phase screen to the branded phase — only progress does, see
670
+ * {@link advanceExternalOverlay}.
671
+ *
672
+ * @param minDisplayMillis how long the overlay is owed on screen before the
673
+ * hand-over may dismiss it (`loading.externalOverlayMinDisplayTime`); defaults
674
+ * to {@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}. Adoption is where the clock
675
+ * starts, so it is taken here rather than read at the hand-over.
676
+ */
677
+ declare function adoptExternalOverlay(overlay: ExternalLoadingOverlay, minDisplayMillis?: number): void;
678
+ /**
679
+ * Report boot progress, as a 0..1 fraction, to a game-supplied overlay.
680
+ *
681
+ * NOT the asset-loading progress the built-in preloader shows — that belongs to
682
+ * the engine's own loading screen, which by then has taken over. This is the
683
+ * pre-first-frame boot: bundle up, Pixi up, SDK handshake done.
684
+ *
685
+ * Sending it is deliberate, not decorative: Artube's loader only crossfades to
686
+ * its BRANDED phase on the first value above zero, so without progress the
687
+ * player never sees the half of their screen the integration exists to show.
688
+ * Values go out as they arrive — the guarantee that the crossfade has room to
689
+ * finish is the dismissal floor ({@link externalOverlayHold}), not a delay
690
+ * here. An earlier design held the first value back for 800ms instead; the
691
+ * floor makes that redundant, and one timer on this overlay is the whole point.
692
+ *
693
+ * Values are clamped to 0..1, kept monotonic, and converted to the PERCENTAGE
694
+ * (0–100) that `ILoaderViewController.updateProgress` documents.
695
+ */
696
+ declare function advanceExternalOverlay(fraction: number): void;
697
+ /**
698
+ * Wait until the overlay has had its guaranteed time on screen. The hand-over
699
+ * awaits this BEFORE it mounts the engine's own loading screen, so the two
700
+ * timelines do not overlap: their screen is whole and undisturbed for the whole
701
+ * window, then ours mounts, paints, and only then takes over.
702
+ *
703
+ * Resolves immediately when no overlay is adopted (every non-Artube target) or
704
+ * when the floor has already passed (any boot slower than it, which is the
705
+ * normal case in production — this costs nothing there).
706
+ *
707
+ * NOT part of the failure path on purpose: {@link releaseExternalOverlay} is
708
+ * synchronous and immediate, so a boot that throws takes the overlay down at
709
+ * once instead of stranding the player behind a courtesy delay. A release also
710
+ * settles a wait already in progress.
711
+ */
712
+ declare function externalOverlayHold(): Promise<void>;
713
+ /**
714
+ * Dismiss the overlay and forget it. Idempotent — this is reached both on the
715
+ * normal hand-over and defensively from the failure paths, and a second
716
+ * `hideLoader()` must not fire.
717
+ *
718
+ * @returns whether this call is the one that dismissed it. Callers that can run
719
+ * BEFORE adoption (`createSlotGame`'s `fatal`, which refuses some launches
720
+ * before `GameApplication` exists) use the `false` to fall back to hiding the
721
+ * game's overlay directly.
722
+ */
723
+ declare function releaseExternalOverlay(): boolean;
724
+
547
725
  /**
548
726
  * Shared Energy8 SVG logo with a loader bar underneath.
549
727
  *
@@ -700,5 +878,5 @@ interface NativeSimulationResult extends SimulationResult {
700
878
  volatilityLabel?: string;
701
879
  }
702
880
 
703
- export { DevBridge, EventEmitter, LOADER_BAR_MAX_WIDTH, PlatformSession, buildLogoSVG, createCSSPreloader, createPlatformSession, removeCSSPreloader, setCSSPreloaderProgress, waitCSSPreloaderTap };
704
- export type { ActionDefinition, AssetBundle, AssetEntry, AssetManifest, BetLevelsConfig, DevBridgeConfig, DistributionBucket, GameDefinition, LoadingScreenConfig, LuaEngineConfig, LuaPlayResult, MaxWinConfig, NativeSimulationConfig, NativeSimulationResult, PersistentStateConfig, PlatformSessionConfig, PlatformSessionEvents, ReplayConfig, ReplayLaunch, SDKOptions, SessionConfig, SimulationConfig, SimulationRawAccumulators, SimulationResult, StageStats, TransitionRule };
881
+ export { DEFAULT_EXTERNAL_MIN_DISPLAY_MS, DevBridge, EventEmitter, LOADER_BAR_MAX_WIDTH, PlatformSession, adoptExternalOverlay, advanceExternalOverlay, buildLogoSVG, createCSSPreloader, createPlatformSession, externalOverlayHold, hasExternalOverlay, releaseExternalOverlay, removeCSSPreloader, setCSSPreloaderProgress, waitCSSPreloaderTap };
882
+ export type { ActionDefinition, AssetBundle, AssetEntry, AssetManifest, BetLevelsConfig, DevBridgeConfig, DistributionBucket, ExternalLoadingOverlay, GameDefinition, LoadingScreenConfig, LuaEngineConfig, LuaPlayResult, MaxWinConfig, NativeSimulationConfig, NativeSimulationResult, PersistentStateConfig, PlatformSessionConfig, PlatformSessionEvents, ReplayConfig, ReplayLaunch, SDKOptions, SessionConfig, SimulationConfig, SimulationRawAccumulators, SimulationResult, StageStats, TransitionRule };
package/dist/index.esm.js CHANGED
@@ -998,40 +998,48 @@ const slottechVariant = {
998
998
  /** Element ids the lifecycle handle binds to. */
999
999
  const RECT_ID = 'ge-vm-loader-rect';
1000
1000
  const TEXT_ID = 'ge-vm-loader-text';
1001
- /** Max width (SVG units) of the voidmoon loader bar fill. Spans the first 'o' → end of the crescent. */
1002
- const LOADER_BAR_MAX_WIDTH = 751;
1001
+ /** Left edge (SVG units) of the loader bar aligned with the "V" of the wordmark. */
1002
+ const LOADER_BAR_X = 249.79;
1003
+ /** Max width (SVG units) of the voidmoon loader bar fill. Spans the wordmark: "V" → end of the final "N". */
1004
+ const LOADER_BAR_MAX_WIDTH = 519;
1003
1005
  /**
1004
- * "voidmoon" wordmark — the official logo, embedded verbatim as SVG outlines:
1005
- * thin white letters with the final "o" of "moon" rendered as a purple crescent
1006
- * (#9D63FE). The glyphs live in a flipped group (`translate(0,941) scale(1,-1)`)
1007
- * exactly as exported; the loader bar + status text are added beneath it in the
1008
- * outer (un-flipped) viewBox space.
1006
+ * voidmoon logo — the official lockup, embedded verbatim as SVG outlines:
1007
+ * the crescent-moon-and-spark mark on the left, the "VOIDMOON" wordmark on the
1008
+ * right, all in #f5f5f5 (filled *and* stroked, exactly as exported the stroke
1009
+ * is what gives the thin glyphs their weight). The export's `.st0` / `.st1`
1010
+ * classes are inlined as presentation attributes on two wrapper groups so the
1011
+ * logo carries no global CSS into the host page.
1012
+ *
1013
+ * The loader bar + status text are added beneath the lockup in the same
1014
+ * viewBox space; the viewBox is taller than the artwork (140.2) to make room.
1009
1015
  */
1010
- const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="301 339 1075 335" class="ge-vm-logo-svg" style="overflow:visible" role="img">
1016
+ const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 769.28 215" class="ge-vm-logo-svg" style="overflow:visible" role="img">
1011
1017
  <title>voidmoon</title>
1012
- <g transform="translate(0,941) scale(1,-1)">
1013
- <g fill="#ffffff" fill-rule="evenodd">
1014
- <path d="M627 562 c-4 -2 -7 -6 -7 -12 0 -13 14 -18 23 -10 3 3 3 5 4 9 0 7 -2 11 -7 13 -5 2 -9 2 -13 0z"/>
1015
- <path d="M780 531 l0 -31 -6 5 c-14 14 -35 17 -56 10 -24 -8 -40 -28 -42 -53 0 -11 1 -19 6 -30 8 -16 22 -27 40 -32 8 -2 23 -2 31 0 18 4 34 17 42 33 2 5 4 11 5 14 1 3 1 24 1 61 l0 55 -10 0 -11 0 0 -32z m-24 -38 c20 -10 28 -31 20 -50 -3 -6 -13 -16 -19 -19 -21 -10 -45 -2 -56 18 -2 4 -2 7 -3 14 -1 14 4 24 15 33 8 6 15 8 27 8 9 -1 10 -1 16 -4z"/>
1016
- <path d="M520 518 c-26 -6 -45 -25 -50 -51 -1 -9 -1 -11 0 -19 3 -12 7 -21 15 -30 8 -8 16 -13 28 -17 6 -2 9 -2 19 -2 10 0 13 0 20 2 21 8 36 24 41 46 1 8 1 13 0 22 -5 23 -21 40 -44 47 -6 2 -23 3 -29 2z m29 -25 c9 -4 16 -11 20 -19 2 -5 3 -6 3 -15 0 -10 -1 -11 -3 -16 -8 -15 -23 -24 -40 -23 -9 1 -15 3 -22 8 -22 15 -21 48 2 63 7 4 14 6 24 6 8 -1 10 -1 16 -4z"/>
1017
- <path d="M869 518 c-21 -4 -35 -18 -40 -38 -1 -6 -1 -14 -1 -43 l1 -35 10 0 11 0 0 37 c0 33 1 38 2 41 3 7 7 11 13 14 5 2 8 3 13 3 11 0 20 -5 25 -16 l3 -5 0 -37 1 -37 10 0 10 0 0 37 c0 35 1 37 3 42 5 10 14 16 26 16 11 0 21 -7 25 -17 2 -5 2 -7 2 -41 0 -19 0 -36 1 -37 0 -1 3 -1 11 -1 l10 1 0 35 c0 23 0 38 -1 42 -2 9 -8 21 -14 27 -20 17 -51 17 -68 -1 l-5 -5 -5 5 c-9 9 -19 13 -31 14 -5 0 -10 0 -12 -1z"/>
1018
- <path d="M1077 517 c-9 -1 -20 -7 -27 -12 -7 -6 -14 -16 -18 -25 -4 -11 -5 -25 -3 -35 5 -21 21 -37 42 -44 38 -12 78 14 81 53 1 15 -4 31 -14 43 -14 17 -38 25 -61 20z m25 -22 c19 -5 31 -24 28 -42 -4 -26 -34 -41 -59 -29 -20 10 -27 32 -17 52 8 16 29 25 48 19z"/>
1019
- <path d="M1282 516 c-18 -5 -34 -21 -38 -39 -1 -4 -1 -18 -1 -40 l1 -35 10 -1 10 0 0 32 c0 20 0 35 1 38 2 11 8 18 18 23 7 3 18 3 26 0 6 -3 12 -9 15 -16 2 -4 3 -6 3 -40 l1 -36 10 0 11 0 0 33 c0 38 0 43 -6 54 -7 14 -19 24 -34 28 -7 1 -20 1 -27 -1z"/>
1020
- <path d="M329 515 c0 -1 2 -5 4 -9 2 -5 13 -28 24 -53 12 -24 21 -45 22 -46 2 -4 10 -7 15 -7 4 0 11 3 13 6 3 2 50 105 50 108 0 1 -3 1 -11 1 l-11 0 -7 -14 c-3 -8 -12 -28 -20 -45 -7 -17 -13 -31 -14 -31 -1 -1 -3 5 -17 35 -19 43 -23 53 -24 54 -1 1 -5 1 -13 1 -6 0 -11 0 -11 0z"/>
1021
- <path d="M623 514 c0 -1 0 -26 0 -57 l1 -55 10 0 10 0 0 56 0 57 -10 0 c-7 0 -10 0 -11 -1z"/>
1022
- </g>
1023
- <g fill="#9D63FE" fill-rule="evenodd">
1024
- <path d="M1150 515 c-3 0 -6 -1 -6 -1 0 -1 2 -2 5 -2 10 -4 26 -17 31 -27 11 -21 8 -44 -7 -62 -5 -7 -17 -16 -24 -18 -3 -1 -5 -2 -4 -2 0 -2 16 -3 24 -3 35 3 59 40 49 74 -2 8 -7 18 -13 24 -5 6 -15 13 -23 16 -8 3 -24 4 -32 1z"/>
1025
- </g>
1018
+ <g fill="#f5f5f5" stroke="#f5f5f5" stroke-miterlimit="10">
1019
+ <polygon points="274.76 98.02 249.79 48.07 256.03 48.07 274.76 85.54 293.49 48.07 299.74 48.07 274.76 98.02"/>
1020
+ <path d="M325.51,70.2h6.24c1.12-8.22,7.63-14.65,15.83-15.85v-6.24c-11.99,1.24-20.85,10.36-22.07,22.1ZM353.82,48.1v6.24c9.03,1.32,15.48,9.27,15.48,18.67,0,10.33-8.4,18.73-18.73,18.73-9.33,0-17.41-6.36-18.82-15.3h-6.24c1.49,12.45,11.94,21.55,25.06,21.55,14.14,0,24.97-11.14,24.97-24.97,0-12.92-8.88-23.58-21.72-24.91Z"/>
1021
+ <rect x="400.39" y="48.08" width="6.24" height="49.95"/>
1022
+ <path d="M437.62,54.32l-6.14-6.24h19.14c13.15,0,23.81,10.85,23.81,24.22v.63c0,13.91-10.99,25.1-24.54,25.1-9.62,0-18.41,0-18.41,0l6.14-6.24h12.27c10.17,0,18.41-8.39,18.41-18.73h0c0-10.34-8.24-18.73-18.41-18.73h-12.27Z"/>
1023
+ <polygon points="499.59 98.02 505.83 98.02 505.85 63.24 524.56 79.29 543.29 64.35 543.29 98.02 549.54 98.02 549.54 48.07 524.62 70.2 499.59 48.07 499.59 98.02"/>
1024
+ <polygon points="725.07 68.91 731.32 74.56 731.32 98.02 725.07 98.02 725.07 68.91"/>
1025
+ <polygon points="768.78 48.07 768.78 98.01 762.53 92.38 753.31 84.06 753.31 84.05 725.07 58.51 725.07 48.1 731.32 53.73 762.53 81.87 762.53 48.07 768.78 48.07"/>
1026
+ <path d="M602.55,48.03v6.24c8.22,1.12,14.65,7.63,15.85,15.83h6.24c-1.24-11.99-10.36-20.85-22.1-22.07ZM624.65,76.34h-6.24c-1.32,9.03-9.27,15.48-18.67,15.48-10.33,0-18.73-8.4-18.73-18.73,0-9.33,6.36-17.41,15.3-18.82v-6.24c-12.45,1.49-21.55,11.94-21.55,25.06,0,14.14,11.14,24.97,24.97,24.97,12.92,0,23.58-8.88,24.91-21.72Z"/>
1027
+ <path d="M699.92,75.89h-6.24c-1.12,8.22-7.63,14.65-15.83,15.85v6.24c11.99-1.24,20.85-10.36,22.07-22.1ZM671.6,97.99v-6.24c-9.03-1.32-15.48-9.27-15.48-18.67,0-10.33,8.4-18.73,18.73-18.73,9.33,0,17.41,6.36,18.82,15.3h6.24c-1.49-12.45-11.94-21.55-25.06-21.55-14.14,0-24.97,11.14-24.97,24.97,0,12.92,8.88,23.58,21.72,24.91Z"/>
1028
+ </g>
1029
+ <g fill="#f5f5f5" stroke="#f5f5f5" stroke-miterlimit="10" stroke-width="2">
1030
+ <path d="M199.39,73.47h.01c-1.76,36.6-31.98,65.73-69.01,65.73s-67.52-29.39-69.04-66.2c-.9-.53-1.53-1.47-1.63-2.57l-22.61,2.67c-1.75.2-3.12,1.6-3.3,3.35l-2.47,24.86-3.11-24.95c-.21-1.7-1.56-3.06-3.28-3.25L.12,70.11l25.29-1.97c1.83-.14,3.28-1.57,3.46-3.38l2.47-25.88,2.47,25.88c.18,1.81,1.63,3.24,3.45,3.38l22.45,1.74c.06-1.14.7-2.13,1.64-2.68C62.87,30.38,93.2,1,130.39,1s67.25,29.12,69.01,65.72h0s-.01,0-.01,0c-1.74-30.76-27.24-55.15-58.43-55.15s-58.52,26.2-58.52,58.52,26.2,58.52,58.52,58.52,56.69-24.4,58.43-55.15Z"/>
1031
+ <path d="M202.84,70.1c0,1.86-1.51,3.38-3.37,3.38-.02,0-.05,0-.07,0,.02-.25.02-.49.03-.74-.01.25-.03.49-.04.74-1.83-.04-3.3-1.53-3.3-3.37s1.47-3.34,3.3-3.37c.01.25.03.49.04.74-.01-.25-.01-.49-.03-.74.02,0,.05,0,.07,0,1.86,0,3.37,1.51,3.37,3.38Z"/>
1032
+ <line x1="232.26" y1="70.1" x2="202.84" y2="70.1"/>
1033
+ <line x1="196.09" y1="70.1" x2="166.58" y2="70.1"/>
1026
1034
  </g>
1027
1035
 
1028
- <rect x="469" y="600" width="751" height="9" rx="4.5" fill="rgba(255,255,255,0.12)"/>
1036
+ <rect x="${LOADER_BAR_X}" y="170" width="${LOADER_BAR_MAX_WIDTH}" height="6.5" rx="3.25" fill="rgba(255,255,255,0.12)"/>
1029
1037
  <clipPath id="vm-loader-clip">
1030
- <rect id="${RECT_ID}" x="469" y="600" width="0" height="9" rx="4.5" class="ge-vm-clip-rect"/>
1038
+ <rect id="${RECT_ID}" x="${LOADER_BAR_X}" y="170" width="0" height="6.5" rx="3.25" class="ge-vm-clip-rect"/>
1031
1039
  </clipPath>
1032
- <rect x="469" y="600" width="751" height="9" rx="4.5" fill="#9D63FE" clip-path="url(#vm-loader-clip)"/>
1040
+ <rect x="${LOADER_BAR_X}" y="170" width="${LOADER_BAR_MAX_WIDTH}" height="6.5" rx="3.25" fill="#9D63FE" clip-path="url(#vm-loader-clip)"/>
1033
1041
 
1034
- <text id="${TEXT_ID}" x="844.5" y="650" text-anchor="middle" class="ge-vm-text">Loading...</text>
1042
+ <text id="${TEXT_ID}" x="${LOADER_BAR_X + LOADER_BAR_MAX_WIDTH / 2}" y="205" text-anchor="middle" class="ge-vm-text">Loading...</text>
1035
1043
  </svg>`;
1036
1044
  const voidmoonVariant = {
1037
1045
  buildContentHTML() {
@@ -1063,7 +1071,7 @@ const voidmoonVariant = {
1063
1071
 
1064
1072
  @keyframes ge-vm-fill {
1065
1073
  0% { width: 0; }
1066
- 50% { width: 751; }
1074
+ 50% { width: 519px; }
1067
1075
  100% { width: 0; }
1068
1076
  }
1069
1077
 
@@ -1075,9 +1083,9 @@ const voidmoonVariant = {
1075
1083
  .ge-vm-text {
1076
1084
  fill: rgba(255, 255, 255, 0.6);
1077
1085
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1078
- font-size: 20px;
1086
+ font-size: 14px;
1079
1087
  font-weight: 600;
1080
- letter-spacing: 3px;
1088
+ letter-spacing: 2.2px;
1081
1089
  animation: ge-vm-pulse 1.5s ease-in-out infinite;
1082
1090
  }
1083
1091
 
@@ -1463,5 +1471,208 @@ function removeCSSPreloader(_container) {
1463
1471
  });
1464
1472
  }
1465
1473
 
1466
- export { DevBridge, EventEmitter, LOADER_BAR_MAX_WIDTH$2 as LOADER_BAR_MAX_WIDTH, PlatformSession, buildLogoSVG, createCSSPreloader, createPlatformSession, removeCSSPreloader, setCSSPreloaderProgress, waitCSSPreloaderTap };
1474
+ let adopted = null;
1475
+ /** Last value pushed, so a repeated milestone cannot walk the bar backwards. */
1476
+ let lastProgress = 0;
1477
+ /** When the overlay was adopted — the origin the minimum display time is measured from. */
1478
+ let adoptedAt = 0;
1479
+ /** How long this overlay is owed on screen; see {@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}. */
1480
+ let minDisplayMs = 0;
1481
+ /** When the first progress went out, i.e. when their phase crossfade started. 0 = not yet. */
1482
+ let crossfadeStartedAt = 0;
1483
+ /** The one pending timer: the hand-over waiting out the floor below. */
1484
+ let holdTimer = null;
1485
+ let holdResolve = null;
1486
+ /**
1487
+ * How long a game-supplied overlay is guaranteed on screen, measured from
1488
+ * adoption (which is the first thing the boot does, so in practice from the
1489
+ * page's first paint).
1490
+ *
1491
+ * Without a floor the overlay lives exactly as long as the gap it covers, and
1492
+ * that gap is short: a warm local boot handed over at ~500ms and dismissed
1493
+ * their screen at ~840ms. A partner's branding that flashes past in under a
1494
+ * second has not been shown. 1500ms is the user's number, and it is also the
1495
+ * smallest one that fits their two-phase screen whole — a 500ms crossfade into
1496
+ * the branded phase plus time to read it — with room for the 300ms dismissal
1497
+ * fade on top.
1498
+ *
1499
+ * Overridable per game: `loading.externalOverlayMinDisplayTime`.
1500
+ */
1501
+ const DEFAULT_EXTERNAL_MIN_DISPLAY_MS = 1500;
1502
+ /**
1503
+ * Artube's partner→branded crossfade, `transition: opacity 0.5s ease` in the
1504
+ * markup their Vite plugin injects (vendored verbatim in
1505
+ * `@energy8platform/artube-server/vite`). It is triggered by the first
1506
+ * `updateProgress` above zero.
1507
+ *
1508
+ * The floor below is extended to cover it because a crossfade that starts and
1509
+ * then gets cut is worse than one that never starts: the player sees a
1510
+ * half-formed gradient and a bar that never fills — the three-screens-at-once
1511
+ * artefact this integration hit once and rejected (evidence:
1512
+ * `rejected-immediate-progress-01-Lartube_P_C.png`). Extending the floor is
1513
+ * bounded by construction: only the FIRST progress starts it, and every
1514
+ * progress value comes from a boot milestone that precedes the hand-over.
1515
+ */
1516
+ const PHASE_CROSSFADE_MS = 500;
1517
+ /**
1518
+ * Call into the game's overlay without letting it break the boot. A third-party
1519
+ * controller is outside our control (Artube's throws if its markup is missing,
1520
+ * for one), and none of these calls is worth failing a game over — least of all
1521
+ * `hideLoader`, where a throw escaping the teardown path is exactly how an
1522
+ * overlay ends up stranded on screen forever.
1523
+ */
1524
+ function guard(method, fn) {
1525
+ try {
1526
+ fn();
1527
+ }
1528
+ catch (err) {
1529
+ console.warn(`[GameEngine] loading overlay ${method}() threw`, err);
1530
+ }
1531
+ }
1532
+ /** Whether a game-supplied overlay is currently on screen and owned by us. */
1533
+ function hasExternalOverlay() {
1534
+ return adopted !== null;
1535
+ }
1536
+ /**
1537
+ * Take ownership of the game's overlay: from here on the engine is responsible
1538
+ * for taking it down, including when the boot fails.
1539
+ *
1540
+ * Called BEFORE anything in the boot that can throw — notably before the
1541
+ * container selector is resolved. Until the engine has adopted it, nothing can
1542
+ * dismiss it, and Artube's is already on screen from `index.html`.
1543
+ *
1544
+ * `showLoader()` is Artube's "reveal the progress affordance"; it does not
1545
+ * switch their two-phase screen to the branded phase — only progress does, see
1546
+ * {@link advanceExternalOverlay}.
1547
+ *
1548
+ * @param minDisplayMillis how long the overlay is owed on screen before the
1549
+ * hand-over may dismiss it (`loading.externalOverlayMinDisplayTime`); defaults
1550
+ * to {@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}. Adoption is where the clock
1551
+ * starts, so it is taken here rather than read at the hand-over.
1552
+ */
1553
+ function adoptExternalOverlay(overlay, minDisplayMillis) {
1554
+ if (adopted)
1555
+ return; // idempotent: boot step 0 and step 2 both reach here
1556
+ adopted = overlay;
1557
+ lastProgress = 0;
1558
+ crossfadeStartedAt = 0;
1559
+ minDisplayMs =
1560
+ typeof minDisplayMillis === 'number' && Number.isFinite(minDisplayMillis)
1561
+ ? Math.max(0, minDisplayMillis)
1562
+ : DEFAULT_EXTERNAL_MIN_DISPLAY_MS;
1563
+ adoptedAt = Date.now();
1564
+ guard('showLoader', () => overlay.showLoader());
1565
+ }
1566
+ /**
1567
+ * Report boot progress, as a 0..1 fraction, to a game-supplied overlay.
1568
+ *
1569
+ * NOT the asset-loading progress the built-in preloader shows — that belongs to
1570
+ * the engine's own loading screen, which by then has taken over. This is the
1571
+ * pre-first-frame boot: bundle up, Pixi up, SDK handshake done.
1572
+ *
1573
+ * Sending it is deliberate, not decorative: Artube's loader only crossfades to
1574
+ * its BRANDED phase on the first value above zero, so without progress the
1575
+ * player never sees the half of their screen the integration exists to show.
1576
+ * Values go out as they arrive — the guarantee that the crossfade has room to
1577
+ * finish is the dismissal floor ({@link externalOverlayHold}), not a delay
1578
+ * here. An earlier design held the first value back for 800ms instead; the
1579
+ * floor makes that redundant, and one timer on this overlay is the whole point.
1580
+ *
1581
+ * Values are clamped to 0..1, kept monotonic, and converted to the PERCENTAGE
1582
+ * (0–100) that `ILoaderViewController.updateProgress` documents.
1583
+ */
1584
+ function advanceExternalOverlay(fraction) {
1585
+ const overlay = adopted;
1586
+ if (!overlay)
1587
+ return;
1588
+ const clamped = Number.isFinite(fraction) ? Math.max(0, Math.min(1, fraction)) : 0;
1589
+ if (clamped <= lastProgress)
1590
+ return;
1591
+ lastProgress = clamped;
1592
+ // The first non-zero value is what starts their partner→branded crossfade, so
1593
+ // it is also what the floor has to cover.
1594
+ if (clamped > 0 && crossfadeStartedAt === 0)
1595
+ crossfadeStartedAt = Date.now();
1596
+ guard('updateProgress', () => overlay.updateProgress(clamped * 100));
1597
+ }
1598
+ /**
1599
+ * Milliseconds still owed to the overlay before the hand-over may dismiss it:
1600
+ * the minimum display time, extended when needed so a phase crossfade that has
1601
+ * started can finish. Zero once both are satisfied — and zero when there is no
1602
+ * overlay, so nothing on a normal target ever waits.
1603
+ */
1604
+ function remainingHoldMs() {
1605
+ if (!adopted)
1606
+ return 0;
1607
+ const now = Date.now();
1608
+ const floor = Math.max(adoptedAt + minDisplayMs, crossfadeStartedAt === 0 ? 0 : crossfadeStartedAt + PHASE_CROSSFADE_MS);
1609
+ return Math.max(0, floor - now);
1610
+ }
1611
+ /**
1612
+ * Wait until the overlay has had its guaranteed time on screen. The hand-over
1613
+ * awaits this BEFORE it mounts the engine's own loading screen, so the two
1614
+ * timelines do not overlap: their screen is whole and undisturbed for the whole
1615
+ * window, then ours mounts, paints, and only then takes over.
1616
+ *
1617
+ * Resolves immediately when no overlay is adopted (every non-Artube target) or
1618
+ * when the floor has already passed (any boot slower than it, which is the
1619
+ * normal case in production — this costs nothing there).
1620
+ *
1621
+ * NOT part of the failure path on purpose: {@link releaseExternalOverlay} is
1622
+ * synchronous and immediate, so a boot that throws takes the overlay down at
1623
+ * once instead of stranding the player behind a courtesy delay. A release also
1624
+ * settles a wait already in progress.
1625
+ */
1626
+ function externalOverlayHold() {
1627
+ // Reaching here means the boot is done and the only thing left is the wait, so the bar is
1628
+ // finished too. Without this it freezes at the last boot milestone (0.85 live) and their screen
1629
+ // is taken away with the bar still short — a bar that never fills, which is the same artefact in
1630
+ // a different costume. Only when a bar is actually on screen: if no progress ever went out the
1631
+ // player is still on the first phase, and starting a crossfade at the hand-over is precisely
1632
+ // what must not happen.
1633
+ if (crossfadeStartedAt !== 0)
1634
+ advanceExternalOverlay(1);
1635
+ const remaining = remainingHoldMs();
1636
+ if (remaining <= 0)
1637
+ return Promise.resolve();
1638
+ return new Promise((resolve) => {
1639
+ const finish = () => {
1640
+ if (holdTimer !== null)
1641
+ clearTimeout(holdTimer);
1642
+ holdTimer = null;
1643
+ holdResolve = null;
1644
+ resolve();
1645
+ };
1646
+ holdResolve = finish;
1647
+ holdTimer = setTimeout(finish, remaining);
1648
+ });
1649
+ }
1650
+ /**
1651
+ * Dismiss the overlay and forget it. Idempotent — this is reached both on the
1652
+ * normal hand-over and defensively from the failure paths, and a second
1653
+ * `hideLoader()` must not fire.
1654
+ *
1655
+ * @returns whether this call is the one that dismissed it. Callers that can run
1656
+ * BEFORE adoption (`createSlotGame`'s `fatal`, which refuses some launches
1657
+ * before `GameApplication` exists) use the `false` to fall back to hiding the
1658
+ * game's overlay directly.
1659
+ */
1660
+ function releaseExternalOverlay() {
1661
+ // Settle a hand-over that is still waiting out the floor, even if there is
1662
+ // nothing to release. This is the failure path: a boot that threw must not
1663
+ // leave a timer holding the process open (Node-side tests) or a promise that
1664
+ // resolves into a teardown that already happened.
1665
+ if (holdResolve)
1666
+ holdResolve();
1667
+ const overlay = adopted;
1668
+ if (!overlay)
1669
+ return false;
1670
+ adopted = null;
1671
+ lastProgress = 0;
1672
+ crossfadeStartedAt = 0;
1673
+ guard('hideLoader', () => overlay.hideLoader());
1674
+ return true;
1675
+ }
1676
+
1677
+ export { DEFAULT_EXTERNAL_MIN_DISPLAY_MS, DevBridge, EventEmitter, LOADER_BAR_MAX_WIDTH$2 as LOADER_BAR_MAX_WIDTH, PlatformSession, adoptExternalOverlay, advanceExternalOverlay, buildLogoSVG, createCSSPreloader, createPlatformSession, externalOverlayHold, hasExternalOverlay, releaseExternalOverlay, removeCSSPreloader, setCSSPreloaderProgress, waitCSSPreloaderTap };
1467
1678
  //# sourceMappingURL=index.esm.js.map