@bongos/core 1.21.70 → 1.21.72
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/.bongos-core.json +123 -63
- package/.claude/skills/goal-create/SKILL.md +21 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +5 -3
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +46 -5
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +388 -377
- package/docs/copy-registry.json +567 -448
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +912 -931
- package/docs/recipes/core-release-pipeline.md +6 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/modules/hall-ui/public/settings-software-update.js +77 -2
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/lifecycle/db-tasks.js +12 -0
- package/modules/lifecycle/goal-spec-task.js +129 -0
- package/modules/lifecycle/routes/goals.js +7 -9
- package/modules/lifecycle/routes/task-write-routes.js +11 -1
- package/modules/lifecycle/upkeep-home.js +55 -0
- package/modules/public-landing/public/projects-hall-live.states.json +7 -1
- package/modules/public-landing/public/projects.html +581 -0
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +43 -0
- package/scripts/gds/claude-materialize.js +125 -31
- package/scripts/gds/claude-md-template.js +47 -10
- package/scripts/gds/cli-lib.js +4 -4
- package/scripts/gds/feedback-send.js +3 -3
- package/scripts/gds/paste-token.js +1 -1
- package/scripts/gds/provision-net.js +1 -8
- package/scripts/gds/provision-units.js +28 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/setup.js +1 -1
- package/scripts/gds/start.js +4 -4
- package/scripts/gds/surface.js +15 -3
- package/scripts/gds/task.js +32 -5
- package/scripts/gds/upgrade-operator.js +73 -0
- package/scripts/gds/upgrade.js +7 -2
- package/scripts/hall-preview/server.js +3 -0
- package/src/bongos/routes/core-update.js +98 -4
- package/src/bongos/routes/feedback.js +11 -1
- package/src/bongos/upgrade-request.js +158 -0
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +195 -18
- package/tests/cli_allowlist_permissions.mjs +6 -2
- package/tests/cli_surface.mjs +2 -2
- package/tests/fixtures/page-ledger.mjs +5 -0
- package/tests/goal_admission_override.mjs +1 -1
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_spec_entry.mjs +61 -0
- package/tests/goal_spec_task.mjs +197 -0
- package/tests/goal_spec_task_db.mjs +95 -0
- package/tests/projects_hub_cloud_hop.mjs +303 -0
- package/tests/provision_settings_apply.mjs +25 -1
- package/tests/software_update.mjs +18 -1
- package/tests/start_env_guard.mjs +2 -2
- package/tests/upgrade_operator.mjs +105 -0
- package/tests/upgrade_request.mjs +348 -0
- package/tests/upkeep_home.mjs +156 -0
|
@@ -444,6 +444,56 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
444
444
|
.autoOpen{display:flex;align-items:flex-start;gap:10px;margin-top:14px;font-size:14px;line-height:1.45;color:var(--ink-soft);cursor:pointer;min-height:24px;}
|
|
445
445
|
.autoOpen input{width:18px;height:18px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--accent);cursor:pointer;}
|
|
446
446
|
|
|
447
|
+
/* ── the done panel's two moments (task 1004578): WAIT — one status line, a
|
|
448
|
+
thin bar and the cloud hop — then ARRIVE — the steps one at a time. The
|
|
449
|
+
panel's data-moment says which; "plain" (a snag, a teardown, a stopped
|
|
450
|
+
watch) is the panel as it always was. While waiting the strip, the locked
|
|
451
|
+
checklist and the later-lines stand down; the sign-in step can be opened
|
|
452
|
+
in place (the one step a founder can do before the project answers). ── */
|
|
453
|
+
.hop{display:none;margin-top:22px;max-width:680px;}
|
|
454
|
+
#panel-done[data-moment="wait"] .hop{display:block;}
|
|
455
|
+
#panel-done[data-moment="wait"] #liveProgress,
|
|
456
|
+
#panel-done[data-moment="wait"] #wizInvite,
|
|
457
|
+
#panel-done[data-moment="wait"] #wizInviteLater,
|
|
458
|
+
#panel-done[data-moment="wait"] #doneModules,
|
|
459
|
+
#panel-done[data-moment="wait"] .samePath,
|
|
460
|
+
#panel-done[data-moment="wait"] .again{display:none;}
|
|
461
|
+
#panel-done[data-moment="wait"]:not(.hopSignin) #doneStepsHead,
|
|
462
|
+
#panel-done[data-moment="wait"]:not(.hopSignin) #humanSteps{display:none;}
|
|
463
|
+
#panel-done[data-moment="wait"].hopSignin .humanSteps>li:not([data-onboard-step="oauth-app"]){display:none;}
|
|
464
|
+
#panel-done[data-moment="wait"].hopSignin .humanSteps>li::before{content:none;}
|
|
465
|
+
.hopStatus{font-size:14px;font-weight:400;line-height:1.45;color:var(--ink-soft);margin:0;min-height:20px;}
|
|
466
|
+
.hopBar{height:3px;background:var(--rule-soft);margin:10px 0 12px;overflow:hidden;}
|
|
467
|
+
.hopBar span{display:block;height:100%;width:0;background:var(--accent);transition:width .8s ease;}
|
|
468
|
+
.hopStage{position:relative;border-radius:14px;overflow:hidden;background:var(--space-deep);}
|
|
469
|
+
.hopStage{transition:opacity .5s ease;}
|
|
470
|
+
.hop.hopFading .hopStage{opacity:0;}
|
|
471
|
+
.hopStage canvas{display:block;width:100%;height:340px;touch-action:none;}
|
|
472
|
+
.hopStage canvas:focus-visible{outline:2px solid var(--focus);outline-offset:-2px;}
|
|
473
|
+
.hopRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;}
|
|
474
|
+
.hopRow .btn{min-width:44px;}
|
|
475
|
+
.hopHint{font-size:13px;color:var(--ink-faint);margin-right:auto;}
|
|
476
|
+
@media (hover:none){.hopHint{display:none;}}
|
|
477
|
+
.hopScore{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-soft);}
|
|
478
|
+
.hopWhile{font-size:14px;color:var(--ink-soft);margin-top:16px;}
|
|
479
|
+
.hopWhile button{min-height:24px;}
|
|
480
|
+
/* reduced motion: the sky stands still and there is nothing to steer */
|
|
481
|
+
.hop.isStill .hopRow{display:none;}
|
|
482
|
+
/* ARRIVE — the strip's three dots stand down (its note stays: it carries the
|
|
483
|
+
address), and the checklist shows one step at a time under a pager */
|
|
484
|
+
#panel-done[data-moment="arrive"] #liveProgress .stages{display:none;}
|
|
485
|
+
#panel-done[data-moment="arrive"] .humanSteps>li:not(.cur){display:none;}
|
|
486
|
+
#panel-done[data-moment="arrive"] .humanSteps>li.cur{border-bottom:1px solid var(--rule-soft);}
|
|
487
|
+
#panel-done[data-moment="arrive"] .humanSteps>li.cur:not(.sdone)::before{content:attr(data-n);}
|
|
488
|
+
.stepPager{display:none;align-items:center;gap:12px;flex-wrap:wrap;margin-top:16px;max-width:680px;}
|
|
489
|
+
#panel-done[data-moment="arrive"] .stepPager{display:flex;}
|
|
490
|
+
.stepCount{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);margin-right:auto;}
|
|
491
|
+
@media (prefers-reduced-motion:no-preference){
|
|
492
|
+
#panel-done[data-moment="arrive"] .humanSteps>li.cur,
|
|
493
|
+
#panel-done[data-moment="arrive"] .doneHead{animation:arriveIn .7s ease both;}
|
|
494
|
+
}
|
|
495
|
+
@keyframes arriveIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
|
|
496
|
+
|
|
447
497
|
/* ── the standup strip (task 2083), drawn as the lifecycle rail (DESIGN.md):
|
|
448
498
|
three hairlines, a 7px dot, one lit step — no plate, no connector, no
|
|
449
499
|
pulse. The vocabulary is the page's fixed one (F22): filed / standing it
|
|
@@ -1939,6 +1989,25 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1939
1989
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
1940
1990
|
and a live region that is itself replaced never announces -->
|
|
1941
1991
|
<div id="liveProgress" role="status" aria-live="polite"></div>
|
|
1992
|
+
<!-- the wait (task 1004578): one status line, a thin bar, and the cloud
|
|
1993
|
+
hop — one cloud per public project on the platform, in its planet's
|
|
1994
|
+
colour. Shown only while data-moment="wait"; under reduced motion
|
|
1995
|
+
the sky is drawn once and stands still. -->
|
|
1996
|
+
<div class="hop" id="hop">
|
|
1997
|
+
<p class="hopStatus" id="hopStatus" role="status" aria-live="polite"></p>
|
|
1998
|
+
<div class="hopBar" aria-hidden="true"><span id="hopBarFill"></span></div>
|
|
1999
|
+
<div class="hopStage">
|
|
2000
|
+
<canvas id="hopCanvas" tabindex="0" role="img" aria-label="A cloud-hopping game to pass the wait: each cloud is a public project on the platform. Arrow keys steer, space jumps."></canvas>
|
|
2001
|
+
</div>
|
|
2002
|
+
<div class="hopRow">
|
|
2003
|
+
<button type="button" class="btn small ghost" id="hopLeft" aria-label="Move left">←</button>
|
|
2004
|
+
<button type="button" class="btn small ghost" id="hopJump">Jump</button>
|
|
2005
|
+
<button type="button" class="btn small ghost" id="hopRight" aria-label="Move right">→</button>
|
|
2006
|
+
<span class="hopHint">Arrows or A and D steer, space jumps.</span>
|
|
2007
|
+
<span class="hopScore" id="hopScore" aria-live="polite"></span>
|
|
2008
|
+
</div>
|
|
2009
|
+
<p class="hopWhile" id="hopWhile" hidden>While you wait, <button type="button" class="linklike" id="hopSignin" aria-expanded="false">set up GitHub sign-in</button>.</p>
|
|
2010
|
+
</div>
|
|
1942
2011
|
<!-- the invite step (task 1002346, BV1.R27; ADR 0249) — OPTIONAL, and it stands
|
|
1943
2012
|
HERE because an invite needs a project that already exists. Hidden until the
|
|
1944
2013
|
created record carries an id and a slug; the script is the only thing that
|
|
@@ -1963,6 +2032,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1963
2032
|
<p class="samePath" id="wizInviteLater"></p>
|
|
1964
2033
|
<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play</h2>
|
|
1965
2034
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
2035
|
+
<!-- the arrival shows the steps one at a time (task 1004578) -->
|
|
2036
|
+
<div class="stepPager" id="stepPager">
|
|
2037
|
+
<span class="stepCount" id="stepCount" aria-live="polite"></span>
|
|
2038
|
+
<button type="button" class="btn small ghost" id="stepBack">← Back</button>
|
|
2039
|
+
<button type="button" class="btn small" id="stepNext">Continue</button>
|
|
2040
|
+
</div>
|
|
1966
2041
|
<!-- the extras row was a default (task 1002342, BV1.R23) — one line saying
|
|
1967
2042
|
where they live afterwards, so accepting them is never a dead end -->
|
|
1968
2043
|
<p class="samePath" id="doneModules"></p>
|
|
@@ -4644,6 +4719,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4644
4719
|
if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
|
|
4645
4720
|
}
|
|
4646
4721
|
if (step === 1) paintDoors();
|
|
4722
|
+
/* the cloud hop's motion runs only while its panel is the one on show
|
|
4723
|
+
(task 1004578) — a start/stop, not a paint: renderDone paints the panel */
|
|
4724
|
+
if (typeof hopStart === 'function') { if ($('panel-done').classList.contains('on')) hopStart(); else hopStop(); }
|
|
4647
4725
|
if (step === 3) { physLoadAccountOnce(); paintChecks(); }
|
|
4648
4726
|
/* the review hosts the old steps' own controls, so it repaints them on entry.
|
|
4649
4727
|
The repo probe is authenticated — never fire it before /me settles (a pre-auth
|
|
@@ -6637,6 +6715,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6637
6715
|
if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
|
|
6638
6716
|
lastStepsHtml = html;
|
|
6639
6717
|
$('humanSteps').innerHTML = html;
|
|
6718
|
+
if (typeof stepsPaint === 'function') stepsPaint();
|
|
6640
6719
|
maybeAutoOpen();
|
|
6641
6720
|
});
|
|
6642
6721
|
}
|
|
@@ -6655,6 +6734,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6655
6734
|
$('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
|
|
6656
6735
|
/* the invite step waits for the hall (task 1004503): until it answers, one line */
|
|
6657
6736
|
paintInviteGate(false);
|
|
6737
|
+
setMoment('wait'); /* the cloud hop until the project answers (task 1004578) */
|
|
6658
6738
|
renderSteps();
|
|
6659
6739
|
}
|
|
6660
6740
|
|
|
@@ -6859,6 +6939,32 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6859
6939
|
if (!el || html === lastProgHtml) return;
|
|
6860
6940
|
lastProgHtml = html;
|
|
6861
6941
|
el.innerHTML = html;
|
|
6942
|
+
if (typeof hopSync === 'function') hopSync(el);
|
|
6943
|
+
}
|
|
6944
|
+
/* The done panel's moment (task 1004578): 'wait' while the project is being
|
|
6945
|
+
stood up (the cloud hop), 'arrive' once it answers, and 'plain' for a snag,
|
|
6946
|
+
a teardown, a settings change, or a project standing with no address (it
|
|
6947
|
+
will never answer, so there is nothing to arrive at) — each keeps the strip
|
|
6948
|
+
as it always was. Same branch order as renderProgress below. */
|
|
6949
|
+
function momentFor(inst) {
|
|
6950
|
+
var s = inst && inst.status, oi = inst && inst.open_intent;
|
|
6951
|
+
/* only the intents renderProgress itself speaks for count — any other
|
|
6952
|
+
(a running standup, say) falls through to the status, as it does there */
|
|
6953
|
+
var spoken = oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested' ? oi.action : '';
|
|
6954
|
+
if (spoken === 'teardown' || spoken === 'settings-apply') return 'plain';
|
|
6955
|
+
if (spoken === 'provision') return 'wait';
|
|
6956
|
+
if (s === 'error' || s === 'tearing_down' || s === 'torn_down') return 'plain';
|
|
6957
|
+
if (s === 'active') {
|
|
6958
|
+
if (!inst.domain) return 'plain';
|
|
6959
|
+
if (addressReady(inst) && inst.last_verified_up) return 'arrive';
|
|
6960
|
+
}
|
|
6961
|
+
return 'wait';
|
|
6962
|
+
}
|
|
6963
|
+
function setMoment(m) {
|
|
6964
|
+
var panel = $('panel-done');
|
|
6965
|
+
if (!panel) return;
|
|
6966
|
+
if (typeof hopMoment === 'function') hopMoment(panel, m);
|
|
6967
|
+
else panel.setAttribute('data-moment', m);
|
|
6862
6968
|
}
|
|
6863
6969
|
/* Renders the current instance state; returns true when the state is terminal
|
|
6864
6970
|
(no further polling wanted). The strip is chosen by the OPEN INTENT first,
|
|
@@ -6866,6 +6972,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6866
6972
|
told to "start another project". */
|
|
6867
6973
|
function renderProgress(inst) {
|
|
6868
6974
|
if (!$('liveProgress')) return true;
|
|
6975
|
+
if (inst) setMoment(momentFor(inst));
|
|
6869
6976
|
var s = inst && inst.status;
|
|
6870
6977
|
var revive = !!(inst && inst.torn_down_at);
|
|
6871
6978
|
var oi = inst && inst.open_intent;
|
|
@@ -6987,6 +7094,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6987
7094
|
setProgress(progressMarkup(1, 1,
|
|
6988
7095
|
'Still working — this tab will stop watching now. Follow it any time on <a href="?view=mine">your projects page</a>.', '', watchFlow));
|
|
6989
7096
|
}
|
|
7097
|
+
setMoment('plain');
|
|
6990
7098
|
stopPolling();
|
|
6991
7099
|
return;
|
|
6992
7100
|
}
|
|
@@ -7036,6 +7144,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7036
7144
|
var m = String(err && err.message || '');
|
|
7037
7145
|
if (m.indexOf('gone:') === 0) {
|
|
7038
7146
|
setProgress(progressMarkup(1, 1, m.slice(5), 'isErr', watchFlow));
|
|
7147
|
+
setMoment('plain');
|
|
7039
7148
|
stopPolling();
|
|
7040
7149
|
return;
|
|
7041
7150
|
}
|
|
@@ -7060,6 +7169,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7060
7169
|
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
|
|
7061
7170
|
lastStepsHtml = '';
|
|
7062
7171
|
lastProgHtml = '';
|
|
7172
|
+
hopReset();
|
|
7063
7173
|
try { sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
7064
7174
|
/* Every key the initial state declares has to be re-declared here. `modules`
|
|
7065
7175
|
is three-valued and null is its "nothing said" sentinel; leaving the key out
|
|
@@ -7095,6 +7205,477 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7095
7205
|
}
|
|
7096
7206
|
wireAgain();
|
|
7097
7207
|
|
|
7208
|
+
/* ══ THE CLOUD HOP (task 1004578) — the wait, made into something to do.
|
|
7209
|
+
One cloud per PUBLIC project on the platform (the featured feed's planets,
|
|
7210
|
+
never a star or a black hole), in that project's planet accent with its
|
|
7211
|
+
name under it, over a full-width "Your planet" cloud that always catches.
|
|
7212
|
+
The owner's rules, from a played prototype: the clouds stand still, the
|
|
7213
|
+
sky stands still (no parallax — both read as nausea), the view does not
|
|
7214
|
+
follow a jump and glides only after a landing (or a fall off the bottom),
|
|
7215
|
+
no shake. Steer left and right, jump; a miss falls to whatever is below.
|
|
7216
|
+
Past the last cloud: "Max projects reached — build more to jump more".
|
|
7217
|
+
|
|
7218
|
+
Motion: the same posture as the map's sky (C9) — the loop never starts
|
|
7219
|
+
under prefers-reduced-motion, and the rest frame (drawn once) is a
|
|
7220
|
+
complete still sky. Colours are read off the page's own tokens at run
|
|
7221
|
+
time, so a re-brand repaints the game too; the only literals are white
|
|
7222
|
+
and black with alpha. The loop stops whenever the canvas is not on
|
|
7223
|
+
screen or the tab is hidden. ══ */
|
|
7224
|
+
var HOP_GAP = 92, HOP_SPD = 4.2, HOP_JUMP = 11.2, HOP_G = 0.45, HOP_H = 340;
|
|
7225
|
+
var hop = { raf: 0, last: 0, since: 0, target: '', w: 0, dpr: 1, col: null, sky: null, font: 'sans-serif',
|
|
7226
|
+
feed: null, clouds: [], best: 0, maxed: false, p: null, cam: 0, camT: 0, L: false, R: false,
|
|
7227
|
+
stars: [], shoot: null, nextShoot: 0, tr: 0, puffs: null, sparks: null };
|
|
7228
|
+
|
|
7229
|
+
function hopRgb(c) {
|
|
7230
|
+
var n = (String(c).match(/-?[\d.]+/g) || []).map(Number);
|
|
7231
|
+
if (/^color\(/.test(String(c))) return [n[0] * 255, n[1] * 255, n[2] * 255].map(Math.round);
|
|
7232
|
+
return [n[0] || 0, n[1] || 0, n[2] || 0];
|
|
7233
|
+
}
|
|
7234
|
+
function hopRead(expr) {
|
|
7235
|
+
var el = document.createElement('span');
|
|
7236
|
+
el.style.color = expr; el.style.display = 'none';
|
|
7237
|
+
$('hop').appendChild(el);
|
|
7238
|
+
var c = hopRgb(getComputedStyle(el).color);
|
|
7239
|
+
el.parentNode.removeChild(el);
|
|
7240
|
+
return c;
|
|
7241
|
+
}
|
|
7242
|
+
function hopMix(a, b, t) { return [0, 1, 2].map(function (i) { return Math.round(a[i] + (b[i] - a[i]) * t); }); }
|
|
7243
|
+
function hopCss(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + (a == null ? 1 : a) + ')'; }
|
|
7244
|
+
var HOP_WHITE = [255, 255, 255];
|
|
7245
|
+
function hopColors() {
|
|
7246
|
+
var c = {};
|
|
7247
|
+
ORB_ACCENT_NAMES.forEach(function (nm) { c[nm] = hopRead('var(--' + nm + ')'); });
|
|
7248
|
+
['space-deep', 'space-mid', 'space-high', 'nebula', 'gold'].forEach(function (nm) { c[nm] = hopRead('var(--' + nm + ')'); });
|
|
7249
|
+
c.base = hopMix(c['gold-soft'], HOP_WHITE, 0.78);
|
|
7250
|
+
c.baseInk = c['space-mid'];
|
|
7251
|
+
c.head = hopMix(c['gold-soft'], HOP_WHITE, 0.55);
|
|
7252
|
+
c.drum = c.terracotta;
|
|
7253
|
+
return c;
|
|
7254
|
+
}
|
|
7255
|
+
|
|
7256
|
+
/* the feed: the same accent rule as the map — the owner's pick, else the
|
|
7257
|
+
position in the origin-sorted FULL catalog — so a project wears the same
|
|
7258
|
+
colour here as in the sky. Fail soft: no feed, no project clouds. */
|
|
7259
|
+
function hopLoadFeed() {
|
|
7260
|
+
if (hop.feed || hop.loading) return;
|
|
7261
|
+
hop.loading = true;
|
|
7262
|
+
fetch(API + '/projects/featured', { credentials: 'same-origin' })
|
|
7263
|
+
.then(function (res) { if (!res.ok) throw new Error('feed'); return res.json(); })
|
|
7264
|
+
.then(function (d) {
|
|
7265
|
+
var rows = ((d && d.projects) || []).slice().sort(function (a, b) { return String(a.origin).localeCompare(String(b.origin)); });
|
|
7266
|
+
hop.feed = rows.map(function (p, i) {
|
|
7267
|
+
var acc = ORB_ACCENT_NAMES.indexOf(p.planet_accent) !== -1 ? p.planet_accent : ORB_ACCENT_NAMES[i % ORB_ACCENT_NAMES.length];
|
|
7268
|
+
return { name: String(p.name || ''), acc: acc, orb: p.orb };
|
|
7269
|
+
}).filter(function (p) { return p.orb === 'planet' && p.name; }).slice(0, 40);
|
|
7270
|
+
hopBuild();
|
|
7271
|
+
})
|
|
7272
|
+
.catch(function () { hop.feed = []; hopBuild(); });
|
|
7273
|
+
}
|
|
7274
|
+
|
|
7275
|
+
function hopBuild() {
|
|
7276
|
+
var w = hop.w || 600;
|
|
7277
|
+
hop.clouds = [{ i: 0, base: true, y: 0 }];
|
|
7278
|
+
var f = 0.5;
|
|
7279
|
+
(hop.feed || []).forEach(function (p, k) {
|
|
7280
|
+
var i = k + 1, cw = Math.max(64, Math.min(110, w * 0.22) - Math.min(24, i));
|
|
7281
|
+
var reach = Math.min(170, w * 0.4), lo = (cw / 2 + 12) / w, hi = 1 - lo, nf, tries = 0;
|
|
7282
|
+
do { nf = f + (Math.random() < 0.5 ? -1 : 1) * (60 + Math.random() * (reach - 60)) / w; tries++; }
|
|
7283
|
+
while ((nf < lo || nf > hi) && tries < 40);
|
|
7284
|
+
f = Math.max(lo, Math.min(hi, nf));
|
|
7285
|
+
hop.clouds.push({ i: i, f: f, cw: cw, y: -i * HOP_GAP, name: p.name, acc: p.acc });
|
|
7286
|
+
});
|
|
7287
|
+
/* no public project at all is the same truth as having climbed them all */
|
|
7288
|
+
hop.best = 0; hop.maxed = hop.feed !== null && hop.clouds.length === 1;
|
|
7289
|
+
hop.p = { x: w / 2, y: -14, vy: 0, on: hop.clouds[0], face: 1 };
|
|
7290
|
+
hop.cam = hop.camT = -HOP_H * 0.8;
|
|
7291
|
+
hopScore();
|
|
7292
|
+
if (!hop.raf) hopDraw(performance.now());
|
|
7293
|
+
}
|
|
7294
|
+
function hopCloudX(c) { return c.base ? 0 : c.f * hop.w - c.cw / 2; }
|
|
7295
|
+
function hopCloudW(c) { return c.base ? hop.w : c.cw; }
|
|
7296
|
+
|
|
7297
|
+
function hopScore() {
|
|
7298
|
+
var n = hop.clouds.length - 1, el = $('hopScore');
|
|
7299
|
+
if (!el) return;
|
|
7300
|
+
if (hop.feed === null || !hop.p) { el.textContent = ''; return; }
|
|
7301
|
+
el.textContent = hop.maxed ? 'Max projects reached — build more to jump more' : 'Best: ' + hop.best + ' of ' + n + (n === 1 ? ' project' : ' projects');
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7304
|
+
/* the still sky, painted once per size: night ground, a nebula haze in
|
|
7305
|
+
the planet colours, the Milky Way, three far planets */
|
|
7306
|
+
function hopSky() {
|
|
7307
|
+
var w = hop.w, h = HOP_H, dpr = hop.dpr, c = hop.col;
|
|
7308
|
+
var cv = document.createElement('canvas');
|
|
7309
|
+
cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
|
|
7310
|
+
var g = cv.getContext('2d');
|
|
7311
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
7312
|
+
var bg = g.createLinearGradient(0, 0, 0, h);
|
|
7313
|
+
bg.addColorStop(0, hopCss(c['space-deep'])); bg.addColorStop(1, hopCss(hopMix(c['space-high'], c.nebula, 0.5)));
|
|
7314
|
+
g.fillStyle = bg; g.fillRect(0, 0, w, h);
|
|
7315
|
+
[[0.22, 0.24, c.sea, 0.32], [0.8, 0.7, c.terracotta, 0.3], [0.56, 0.1, c.nebula, 0.6], [0.92, 0.16, c['gold-soft'], 0.2]].forEach(function (n) {
|
|
7316
|
+
var x = n[0] * w, y = n[1] * h, r = Math.max(w, h) * n[3];
|
|
7317
|
+
var rg = g.createRadialGradient(x, y, 0, x, y, r);
|
|
7318
|
+
rg.addColorStop(0, hopCss(n[2], 0.17)); rg.addColorStop(1, hopCss(n[2], 0));
|
|
7319
|
+
g.fillStyle = rg; g.fillRect(0, 0, w, h);
|
|
7320
|
+
});
|
|
7321
|
+
var dots = Math.round(w * 2.6);
|
|
7322
|
+
for (var k = 0; k < dots; k++) {
|
|
7323
|
+
var t = Math.random(), off = (Math.random() + Math.random() + Math.random() - 1.5) * h * 0.2;
|
|
7324
|
+
var tint = Math.random() < 0.5 ? HOP_WHITE : hopMix(c.gold, HOP_WHITE, 0.6);
|
|
7325
|
+
g.fillStyle = hopCss(tint, 0.15 + Math.random() * 0.4);
|
|
7326
|
+
var s = Math.random() < 0.92 ? 1 : 1.6;
|
|
7327
|
+
g.fillRect(t * w + off * 0.5, h * 0.88 - t * h * 0.78 + off, s, s);
|
|
7328
|
+
}
|
|
7329
|
+
for (k = 0; k < w * 0.6; k++) { g.fillStyle = hopCss(HOP_WHITE, 0.1 + Math.random() * 0.3); g.fillRect(Math.random() * w, Math.random() * h, 1, 1); }
|
|
7330
|
+
function planet(x, y, r, col, ring, moon) {
|
|
7331
|
+
var pg = g.createRadialGradient(x - r * 0.4, y - r * 0.4, r * 0.1, x, y, r);
|
|
7332
|
+
pg.addColorStop(0, hopCss(hopMix(col, HOP_WHITE, 0.85))); pg.addColorStop(0.3, hopCss(col)); pg.addColorStop(1, hopCss(c['space-deep']));
|
|
7333
|
+
g.strokeStyle = hopCss(HOP_WHITE, 0.35); g.lineWidth = 2;
|
|
7334
|
+
if (ring) { g.beginPath(); g.ellipse(x, y, r * 1.8, r * 0.45, -0.35, Math.PI, 2 * Math.PI); g.stroke(); }
|
|
7335
|
+
g.fillStyle = pg; g.beginPath(); g.arc(x, y, r, 0, 7); g.fill();
|
|
7336
|
+
if (ring) { g.beginPath(); g.ellipse(x, y, r * 1.8, r * 0.45, -0.35, 0, Math.PI); g.stroke(); }
|
|
7337
|
+
if (moon) { g.fillStyle = hopCss(hopMix(c.nebula, HOP_WHITE, 0.8)); g.beginPath(); g.arc(x + r * 2.2, y - r * 0.8, r * 0.3, 0, 7); g.fill(); }
|
|
7338
|
+
}
|
|
7339
|
+
planet(w * 0.13, h * 0.19, Math.min(28, w * 0.05), c.sea, true, false);
|
|
7340
|
+
planet(w * 0.88, h * 0.76, Math.min(16, w * 0.03), c.terracotta, false, true);
|
|
7341
|
+
planet(w * 0.68, h * 0.25, Math.min(10, w * 0.02), c['gold-soft'], false, false);
|
|
7342
|
+
hop.sky = cv;
|
|
7343
|
+
hop.stars = [];
|
|
7344
|
+
for (k = 0; k < Math.round(w / 6); k++) {
|
|
7345
|
+
hop.stars.push({ x: Math.random() * w, y: Math.random() * h, s: 0.6 + Math.random() * 1.6, ph: Math.random() * 6.28,
|
|
7346
|
+
sp: 0.5 + Math.random() * 0.8, tint: [HOP_WHITE, hopMix(c.gold, HOP_WHITE, 0.5), hopMix(c.sea, HOP_WHITE, 0.6)][k % 3], flare: Math.random() < 0.08 });
|
|
7347
|
+
}
|
|
7348
|
+
}
|
|
7349
|
+
|
|
7350
|
+
/* size the canvas to its stage; the clouds keep their place as a fraction
|
|
7351
|
+
of the width, the player keeps theirs */
|
|
7352
|
+
function hopSize() {
|
|
7353
|
+
var cv = $('hopCanvas');
|
|
7354
|
+
var w = Math.round(cv.clientWidth);
|
|
7355
|
+
if (!w) return false;
|
|
7356
|
+
var dpr = window.devicePixelRatio || 1;
|
|
7357
|
+
if (w === hop.w && dpr === hop.dpr && hop.sky) return true;
|
|
7358
|
+
if (hop.p && hop.w) hop.p.x = hop.p.x * w / hop.w;
|
|
7359
|
+
hop.w = w; hop.dpr = dpr;
|
|
7360
|
+
cv.width = Math.round(w * dpr); cv.height = Math.round(HOP_H * dpr);
|
|
7361
|
+
if (!hop.col) hop.col = hopColors();
|
|
7362
|
+
hop.font = getComputedStyle(cv).fontFamily || 'sans-serif';
|
|
7363
|
+
hopSky();
|
|
7364
|
+
if (!hop.p) hopBuild();
|
|
7365
|
+
return true;
|
|
7366
|
+
}
|
|
7367
|
+
|
|
7368
|
+
function hopRunning() {
|
|
7369
|
+
var cv = $('hopCanvas');
|
|
7370
|
+
return !REDUCED && !document.hidden && hop.target === 'wait' && !!cv && cv.offsetWidth > 0;
|
|
7371
|
+
}
|
|
7372
|
+
function hopStart() {
|
|
7373
|
+
var hopEl = $('hop');
|
|
7374
|
+
if (!hop || !hopEl) return; /* hoisted: show() can ask before the hop is declared */
|
|
7375
|
+
hopEl.classList.toggle('isStill', REDUCED);
|
|
7376
|
+
hopLoadFeed();
|
|
7377
|
+
if (!hopSize()) return;
|
|
7378
|
+
if (!hopRunning()) { if (!hop.raf) hopDraw(performance.now()); return; }
|
|
7379
|
+
if (hop.raf) return;
|
|
7380
|
+
hop.last = performance.now();
|
|
7381
|
+
if (!hop.since) hop.since = hop.last;
|
|
7382
|
+
hop.raf = requestAnimationFrame(hopFrame);
|
|
7383
|
+
}
|
|
7384
|
+
function hopStop() { if (!hop) return; if (hop.raf) { cancelAnimationFrame(hop.raf); hop.raf = 0; } hop.L = hop.R = false; }
|
|
7385
|
+
|
|
7386
|
+
function hopJump() {
|
|
7387
|
+
if (hop.p && hop.p.on && !hop.maxed && !hop.tr && hop.target === 'wait') { hop.p.vy = -HOP_JUMP; hop.p.on = null; }
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7390
|
+
function hopStep(k) {
|
|
7391
|
+
var p = hop.p, w = hop.w;
|
|
7392
|
+
if (!p) return;
|
|
7393
|
+
var mv = (hop.R ? 1 : 0) - (hop.L ? 1 : 0);
|
|
7394
|
+
if (mv) p.face = mv;
|
|
7395
|
+
p.x = Math.max(10, Math.min(w - 10, p.x + mv * HOP_SPD * k));
|
|
7396
|
+
if (p.on) {
|
|
7397
|
+
p.y = p.on.y - 14;
|
|
7398
|
+
var ox = hopCloudX(p.on);
|
|
7399
|
+
if (!p.on.base && (p.x < ox - 6 || p.x > ox + hopCloudW(p.on) + 6)) { p.on = null; p.vy = 0; }
|
|
7400
|
+
}
|
|
7401
|
+
if (!p.on) {
|
|
7402
|
+
var py = p.y;
|
|
7403
|
+
p.vy = Math.min(p.vy + HOP_G * k, 14);
|
|
7404
|
+
p.y += p.vy * k;
|
|
7405
|
+
if (p.vy > 0) {
|
|
7406
|
+
for (var i = 0; i < hop.clouds.length; i++) {
|
|
7407
|
+
var c = hop.clouds[i], top = c.y - 14, cx = hopCloudX(c);
|
|
7408
|
+
if (py <= top && p.y >= top && (c.base || (p.x > cx - 6 && p.x < cx + c.cw + 6))) {
|
|
7409
|
+
p.on = c; p.y = top; p.vy = 0;
|
|
7410
|
+
if (c.i > hop.best) {
|
|
7411
|
+
hop.best = c.i;
|
|
7412
|
+
if (hop.best === hop.clouds.length - 1) hop.maxed = true;
|
|
7413
|
+
hopScore();
|
|
7414
|
+
}
|
|
7415
|
+
break;
|
|
7416
|
+
}
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
if (p.on) hop.camT = p.on.y - HOP_H * (p.on.base ? 0.8 : 0.62);
|
|
7421
|
+
else if (p.y - hop.camT > HOP_H - 30) hop.camT = p.y - HOP_H + 30;
|
|
7422
|
+
hop.cam += (hop.camT - hop.cam) * Math.min(1, 0.07 * k);
|
|
7423
|
+
}
|
|
7424
|
+
|
|
7425
|
+
function hopPuff(g, x, y, w, col, name) {
|
|
7426
|
+
g.fillStyle = hopCss(col);
|
|
7427
|
+
[[0.5, 6, 0.5, 10], [0.32, -2, 0.22, 13], [0.62, -5, 0.26, 16]].forEach(function (e) {
|
|
7428
|
+
g.beginPath(); g.ellipse(x + w * e[0], y + e[1], w * e[2], e[3], 0, 0, 7); g.fill();
|
|
7429
|
+
});
|
|
7430
|
+
if (name) {
|
|
7431
|
+
g.fillStyle = hopCss(HOP_WHITE, 0.82); g.font = '11px ' + hop.font; g.textAlign = 'center';
|
|
7432
|
+
g.fillText(name.length > 18 ? name.slice(0, 17) + '…' : name, x + w / 2, y + 28);
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
function hopBase(g, y) {
|
|
7436
|
+
var c = hop.col, w = hop.w;
|
|
7437
|
+
g.fillStyle = hopCss(c.base);
|
|
7438
|
+
/* one path per bump: ellipses sharing a path are joined by stray chords */
|
|
7439
|
+
for (var k = 0; k * 80 < w + 80; k++) { g.beginPath(); g.ellipse(k * 80 + 20, y + 8 + (k % 2) * 6, 66, 24, 0, 0, 7); g.fill(); }
|
|
7440
|
+
g.fillRect(0, y + 10, w, HOP_H);
|
|
7441
|
+
g.fillStyle = hopCss(c.baseInk); g.font = '12px ' + hop.font; g.textAlign = 'center';
|
|
7442
|
+
g.fillText('Your planet', w / 2, y + 38);
|
|
7443
|
+
}
|
|
7444
|
+
function hopPlayer(g, x, y, f) {
|
|
7445
|
+
var c = hop.col;
|
|
7446
|
+
g.fillStyle = hopCss(c.drum);
|
|
7447
|
+
g.fillRect(x - 14, y - 10, 12, 13); g.fillRect(x + 2, y - 7, 11, 10);
|
|
7448
|
+
g.fillStyle = hopCss(c.head);
|
|
7449
|
+
g.beginPath(); g.ellipse(x - 8, y - 10, 6, 2.6, 0, 0, 7); g.fill();
|
|
7450
|
+
g.beginPath(); g.ellipse(x + 7.5, y - 7, 5.5, 2.4, 0, 0, 7); g.fill();
|
|
7451
|
+
g.fillStyle = hopCss(HOP_WHITE, 0.7);
|
|
7452
|
+
g.fillRect(x + (f > 0 ? 3 : -11), y - 3, 2, 2);
|
|
7453
|
+
}
|
|
7454
|
+
|
|
7455
|
+
function hopDraw(now) {
|
|
7456
|
+
var cv = $('hopCanvas');
|
|
7457
|
+
if (!cv || !hop.sky || !hop.col) return;
|
|
7458
|
+
var g = cv.getContext('2d'), w = hop.w, h = HOP_H, c = hop.col;
|
|
7459
|
+
g.setTransform(hop.dpr, 0, 0, hop.dpr, 0, 0);
|
|
7460
|
+
g.drawImage(hop.sky, 0, 0, w, h);
|
|
7461
|
+
var t = now / 1000, still = REDUCED;
|
|
7462
|
+
hop.stars.forEach(function (s) {
|
|
7463
|
+
var a = still ? 0.75 : 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(t * s.sp + s.ph));
|
|
7464
|
+
g.fillStyle = hopCss(s.tint, a); g.beginPath(); g.arc(s.x, s.y, s.s * 0.6, 0, 7); g.fill();
|
|
7465
|
+
if (s.flare) { g.fillStyle = hopCss(s.tint, a * 0.5); g.fillRect(s.x - s.s * 3, s.y - 0.5, s.s * 6, 1); g.fillRect(s.x - 0.5, s.y - s.s * 3, 1, s.s * 6); }
|
|
7466
|
+
});
|
|
7467
|
+
if (!still) {
|
|
7468
|
+
if (!hop.shoot && now > hop.nextShoot) {
|
|
7469
|
+
var fromL = Math.random() < 0.5;
|
|
7470
|
+
hop.shoot = { x: fromL ? Math.random() * w * 0.4 : w * (0.6 + Math.random() * 0.4), y: 10 + Math.random() * h * 0.35,
|
|
7471
|
+
vx: (fromL ? 1 : -1) * (9 + Math.random() * 3), vy: 2.5 + Math.random() * 1.5, life: 0 };
|
|
7472
|
+
}
|
|
7473
|
+
if (hop.shoot) {
|
|
7474
|
+
var sh = hop.shoot; sh.life++; sh.x += sh.vx; sh.y += sh.vy;
|
|
7475
|
+
var sa = Math.max(0, 1 - sh.life / 45);
|
|
7476
|
+
var sg = g.createLinearGradient(sh.x, sh.y, sh.x - sh.vx * 9, sh.y - sh.vy * 9);
|
|
7477
|
+
sg.addColorStop(0, hopCss(hopMix(c.gold, HOP_WHITE, 0.7), sa)); sg.addColorStop(1, hopCss(HOP_WHITE, 0));
|
|
7478
|
+
g.strokeStyle = sg; g.lineWidth = 2; g.beginPath(); g.moveTo(sh.x, sh.y); g.lineTo(sh.x - sh.vx * 9, sh.y - sh.vy * 9); g.stroke();
|
|
7479
|
+
if (sh.life > 45) { hop.shoot = null; hop.nextShoot = now + 3500 + Math.random() * 3500; }
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
hop.clouds.forEach(function (cl) {
|
|
7483
|
+
var y = cl.y - hop.cam;
|
|
7484
|
+
if (y < -40 || y > h + 60) return;
|
|
7485
|
+
if (cl.base) hopBase(g, y); else hopPuff(g, hopCloudX(cl), y, cl.cw, c[cl.acc] || c.accent, cl.name);
|
|
7486
|
+
});
|
|
7487
|
+
if (hop.p) hopPlayer(g, hop.p.x, hop.p.y - hop.cam, hop.p.face);
|
|
7488
|
+
if (hop.maxed && hop.feed !== null && !hop.tr) {
|
|
7489
|
+
/* a band across the top of the sky — the player stands on the last cloud below it */
|
|
7490
|
+
g.fillStyle = 'rgba(0,0,0,0.6)'; g.fillRect(0, 18, w, 76);
|
|
7491
|
+
g.textAlign = 'center';
|
|
7492
|
+
g.fillStyle = hopCss(c['gold-soft']); g.font = '500 19px ' + hop.font; g.fillText('Max projects reached', w / 2, 50);
|
|
7493
|
+
g.fillStyle = hopCss(HOP_WHITE, 0.88); g.font = '14px ' + hop.font; g.fillText('Build more to jump more', w / 2, 76);
|
|
7494
|
+
}
|
|
7495
|
+
if (hop.tr) hopVeil(g, (now - hop.tr) / 1000);
|
|
7496
|
+
}
|
|
7497
|
+
|
|
7498
|
+
/* the arrival's curtain: planet-coloured clouds sweep in from every edge
|
|
7499
|
+
with the stars twinkling over them until they fill the stage */
|
|
7500
|
+
function hopEase(t) { t = Math.max(0, Math.min(1, t)); return 1 - Math.pow(1 - t, 3); }
|
|
7501
|
+
function hopVeil(g, t) {
|
|
7502
|
+
hop.sparks.forEach(function (s) {
|
|
7503
|
+
var k = t - s.dl; if (k < 0) return;
|
|
7504
|
+
var a = Math.max(0, Math.sin(k * 5)) * Math.min(1, k * 3);
|
|
7505
|
+
g.fillStyle = hopCss(HOP_WHITE, a);
|
|
7506
|
+
g.fillRect(s.x - s.s / 2, s.y - s.s / 2, s.s, s.s); g.fillRect(s.x - s.s * 2, s.y - 0.5, s.s * 4, 1); g.fillRect(s.x - 0.5, s.y - s.s * 2, 1, s.s * 4);
|
|
7507
|
+
});
|
|
7508
|
+
hop.puffs.forEach(function (q) {
|
|
7509
|
+
var k = hopEase((t - q.dl) / 1.1); if (k <= 0) return;
|
|
7510
|
+
var px = q.sx + (q.tx - q.sx) * k, py = q.sy + (q.ty - q.sy) * k;
|
|
7511
|
+
g.fillStyle = hopCss(q.col);
|
|
7512
|
+
g.beginPath(); g.ellipse(px, py, q.r * k, q.r * 0.6 * k, 0, 0, 7); g.fill();
|
|
7513
|
+
g.beginPath(); g.ellipse(px - q.r * 0.5 * k, py + q.r * 0.15 * k, q.r * 0.6 * k, q.r * 0.45 * k, 0, 0, 7); g.fill();
|
|
7514
|
+
});
|
|
7515
|
+
}
|
|
7516
|
+
function hopCurtain(done) {
|
|
7517
|
+
var w = hop.w, h = HOP_H, c = hop.col;
|
|
7518
|
+
var cols = ORB_ACCENT_NAMES.map(function (nm) { return c[nm]; });
|
|
7519
|
+
hop.puffs = []; hop.sparks = [];
|
|
7520
|
+
for (var k = 0; k < 70; k++) {
|
|
7521
|
+
var a = Math.random() * 6.28, d = Math.max(w, h) * 0.75 + Math.random() * 120;
|
|
7522
|
+
hop.puffs.push({ tx: Math.random() * w, ty: Math.random() * h, sx: w / 2 + Math.cos(a) * d, sy: h / 2 + Math.sin(a) * d,
|
|
7523
|
+
r: 50 + Math.random() * 70, col: Math.random() < 0.35 ? c.base : cols[k % cols.length], dl: Math.random() * 0.5 });
|
|
7524
|
+
}
|
|
7525
|
+
for (k = 0; k < 90; k++) hop.sparks.push({ x: Math.random() * w, y: Math.random() * h, dl: Math.random() * 0.9, s: 1 + Math.random() * 2.5 });
|
|
7526
|
+
hop.L = hop.R = false;
|
|
7527
|
+
hop.tr = performance.now();
|
|
7528
|
+
hop.trDone = done;
|
|
7529
|
+
}
|
|
7530
|
+
|
|
7531
|
+
function hopFrame(now) {
|
|
7532
|
+
hop.raf = 0;
|
|
7533
|
+
if (hop.tr) {
|
|
7534
|
+
hopDraw(now);
|
|
7535
|
+
/* the filled stage fades out, then the arrival fades in */
|
|
7536
|
+
if (now - hop.tr > 1250) $('hop').classList.add('hopFading');
|
|
7537
|
+
if (now - hop.tr > 1800) { var done = hop.trDone; hop.tr = 0; hop.trDone = null; if (done) done(); return; }
|
|
7538
|
+
hop.raf = requestAnimationFrame(hopFrame);
|
|
7539
|
+
return;
|
|
7540
|
+
}
|
|
7541
|
+
if (!hopRunning()) { hopDraw(now); return; }
|
|
7542
|
+
hopSize();
|
|
7543
|
+
var k = Math.min(2.5, Math.max(0.2, (now - hop.last) / (1000 / 60)));
|
|
7544
|
+
hop.last = now;
|
|
7545
|
+
hopStep(k);
|
|
7546
|
+
hopDraw(now);
|
|
7547
|
+
hop.raf = requestAnimationFrame(hopFrame);
|
|
7548
|
+
}
|
|
7549
|
+
|
|
7550
|
+
/* the panel's moment, from renderProgress (setMoment). Arriving from a
|
|
7551
|
+
game that has been running gets the curtain first; a resume onto a
|
|
7552
|
+
project that is already live, or reduced motion, switches straight. */
|
|
7553
|
+
function hopMoment(panel, m) {
|
|
7554
|
+
if (!hop) { panel.setAttribute('data-moment', m); return; }
|
|
7555
|
+
if (hop.target === m) return;
|
|
7556
|
+
var was = hop.target;
|
|
7557
|
+
hop.target = m;
|
|
7558
|
+
if (hop.tr) return; /* the curtain is up — it applies the LATEST target when it lands */
|
|
7559
|
+
function apply() {
|
|
7560
|
+
panel.setAttribute('data-moment', hop.target);
|
|
7561
|
+
$('hop').classList.remove('hopFading');
|
|
7562
|
+
if (hop.target === 'wait') hopStart(); else hopStop();
|
|
7563
|
+
if (hop.target === 'arrive') stepsPaint(true);
|
|
7564
|
+
if (hop.target !== 'wait') { panel.classList.remove('hopSignin'); $('hopSignin').setAttribute('aria-expanded', 'false'); }
|
|
7565
|
+
}
|
|
7566
|
+
if (m === 'arrive' && was === 'wait' && !REDUCED && hop.raf && performance.now() - hop.since > 2500) {
|
|
7567
|
+
cancelAnimationFrame(hop.raf); hop.raf = 0;
|
|
7568
|
+
hopCurtain(apply);
|
|
7569
|
+
hop.raf = requestAnimationFrame(hopFrame);
|
|
7570
|
+
return;
|
|
7571
|
+
}
|
|
7572
|
+
apply();
|
|
7573
|
+
}
|
|
7574
|
+
function hopReset() {
|
|
7575
|
+
if (!hop) return;
|
|
7576
|
+
hopStop();
|
|
7577
|
+
hop.target = ''; hop.since = 0; hop.tr = 0; hop.trDone = null; hop.p = null;
|
|
7578
|
+
hop.best = 0; hop.maxed = false;
|
|
7579
|
+
if (hop.w) hopBuild();
|
|
7580
|
+
stepIdx = -1; stepAuto = true;
|
|
7581
|
+
var panel = $('panel-done');
|
|
7582
|
+
panel.removeAttribute('data-moment'); panel.classList.remove('hopSignin');
|
|
7583
|
+
}
|
|
7584
|
+
|
|
7585
|
+
/* the status line and the bar read what the strip already says — one
|
|
7586
|
+
vocabulary, one source (renderProgress), never a second copy */
|
|
7587
|
+
function hopSync(el) {
|
|
7588
|
+
var st = $('hopStatus');
|
|
7589
|
+
if (!st || !el) return;
|
|
7590
|
+
var note = el.querySelector('.pnote');
|
|
7591
|
+
var text = note ? note.textContent : '';
|
|
7592
|
+
if (st.textContent !== text) st.textContent = text;
|
|
7593
|
+
var lis = el.querySelectorAll('.stages li').length;
|
|
7594
|
+
var done = el.querySelectorAll('.stages li.done').length + (el.querySelector('.stages li.now') ? 0.5 : 0);
|
|
7595
|
+
$('hopBarFill').style.width = Math.max(4, lis ? Math.min(100, done / lis * 100) : 0) + '%';
|
|
7596
|
+
}
|
|
7597
|
+
|
|
7598
|
+
/* the arrival's steps, one at a time. The list is still rendered whole
|
|
7599
|
+
(renderSteps, change-only); this marks the one on show, numbers it, and
|
|
7600
|
+
paints the pager. Until the founder pages for themselves, the step on
|
|
7601
|
+
show follows the first one not yet done — the checklist ticks itself, so
|
|
7602
|
+
a step finishing moves them on. Once they page, their choice holds. */
|
|
7603
|
+
var stepIdx = -1, stepAuto = true;
|
|
7604
|
+
function stepsPaint(fresh) {
|
|
7605
|
+
var panel = $('panel-done'), list = $('humanSteps');
|
|
7606
|
+
if (!panel || !list) return;
|
|
7607
|
+
var items = Array.prototype.slice.call(list.children);
|
|
7608
|
+
var oauth = list.querySelector('[data-onboard-step="oauth-app"]');
|
|
7609
|
+
var open = panel.classList.contains('hopSignin');
|
|
7610
|
+
var can = !!oauth && !oauth.classList.contains('slocked') && !oauth.classList.contains('sdone');
|
|
7611
|
+
$('hopWhile').hidden = !(can || open);
|
|
7612
|
+
if (!items.length) { $('stepCount').textContent = ''; $('stepBack').hidden = true; $('stepNext').hidden = true; return; }
|
|
7613
|
+
if (fresh) stepAuto = true;
|
|
7614
|
+
if (stepAuto || !(stepIdx >= 0)) {
|
|
7615
|
+
stepIdx = 0;
|
|
7616
|
+
for (var i = 0; i < items.length; i++) { if (!items[i].classList.contains('sdone')) { stepIdx = i; break; } }
|
|
7617
|
+
}
|
|
7618
|
+
stepIdx = Math.max(0, Math.min(items.length - 1, stepIdx));
|
|
7619
|
+
items.forEach(function (li, j) {
|
|
7620
|
+
li.classList.toggle('cur', j === stepIdx);
|
|
7621
|
+
/* a hidden step never counts, so the one on show carries its own number */
|
|
7622
|
+
li.setAttribute('data-n', (j < 9 ? '0' : '') + (j + 1));
|
|
7623
|
+
});
|
|
7624
|
+
$('stepCount').textContent = 'Step ' + (stepIdx + 1) + ' of ' + items.length;
|
|
7625
|
+
$('stepBack').hidden = stepIdx <= 0;
|
|
7626
|
+
$('stepNext').hidden = stepIdx >= items.length - 1;
|
|
7627
|
+
}
|
|
7628
|
+
function stepGo(d) {
|
|
7629
|
+
stepAuto = false;
|
|
7630
|
+
stepIdx += d;
|
|
7631
|
+
stepsPaint();
|
|
7632
|
+
var cur = $('humanSteps').querySelector('li.cur');
|
|
7633
|
+
if (cur) { cur.setAttribute('tabindex', '-1'); cur.focus(); }
|
|
7634
|
+
}
|
|
7635
|
+
$('stepNext').addEventListener('click', function () { stepGo(1); });
|
|
7636
|
+
$('stepBack').addEventListener('click', function () { stepGo(-1); });
|
|
7637
|
+
|
|
7638
|
+
$('hopSignin').addEventListener('click', function () {
|
|
7639
|
+
var panel = $('panel-done');
|
|
7640
|
+
var open = !panel.classList.contains('hopSignin');
|
|
7641
|
+
panel.classList.toggle('hopSignin', open);
|
|
7642
|
+
this.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
7643
|
+
var li = $('humanSteps').querySelector('[data-onboard-step="oauth-app"]');
|
|
7644
|
+
if (open && li) { li.setAttribute('tabindex', '-1'); li.focus(); }
|
|
7645
|
+
});
|
|
7646
|
+
|
|
7647
|
+
(function hopInput() {
|
|
7648
|
+
var cv = $('hopCanvas');
|
|
7649
|
+
function key(ev, down) {
|
|
7650
|
+
var k = ev.code;
|
|
7651
|
+
if (k === 'Space' || k === 'ArrowUp' || k === 'KeyW') { ev.preventDefault(); if (down) hopJump(); }
|
|
7652
|
+
else if (k === 'ArrowLeft' || k === 'KeyA') { ev.preventDefault(); hop.L = down; }
|
|
7653
|
+
else if (k === 'ArrowRight' || k === 'KeyD') { ev.preventDefault(); hop.R = down; }
|
|
7654
|
+
}
|
|
7655
|
+
cv.addEventListener('keydown', function (ev) { key(ev, true); });
|
|
7656
|
+
cv.addEventListener('keyup', function (ev) { key(ev, false); });
|
|
7657
|
+
cv.addEventListener('blur', function () { hop.L = hop.R = false; });
|
|
7658
|
+
cv.addEventListener('pointerdown', function (ev) {
|
|
7659
|
+
cv.focus();
|
|
7660
|
+
var r = cv.getBoundingClientRect(), fx = (ev.clientX - r.left) / (r.width || 1);
|
|
7661
|
+
if (fx < 0.33) hop.L = true; else if (fx > 0.67) hop.R = true; else hopJump();
|
|
7662
|
+
});
|
|
7663
|
+
['pointerup', 'pointerleave', 'pointercancel'].forEach(function (t) { cv.addEventListener(t, function () { hop.L = hop.R = false; }); });
|
|
7664
|
+
/* the on-screen pad: hold to steer; a keyboard press on a pad button nudges */
|
|
7665
|
+
function pad(id, side) {
|
|
7666
|
+
var b = $(id);
|
|
7667
|
+
b.addEventListener('pointerdown', function (ev) { ev.preventDefault(); hop[side] = true; });
|
|
7668
|
+
['pointerup', 'pointerleave', 'pointercancel'].forEach(function (t) { b.addEventListener(t, function () { hop[side] = false; }); });
|
|
7669
|
+
b.addEventListener('click', function (ev) {
|
|
7670
|
+
if (ev.detail === 0 && hop.p) hop.p.x = Math.max(10, Math.min(hop.w - 10, hop.p.x + (side === 'R' ? 36 : -36)));
|
|
7671
|
+
});
|
|
7672
|
+
}
|
|
7673
|
+
pad('hopLeft', 'L'); pad('hopRight', 'R');
|
|
7674
|
+
$('hopJump').addEventListener('click', hopJump);
|
|
7675
|
+
document.addEventListener('visibilitychange', function () { if (!document.hidden) hopStart(); });
|
|
7676
|
+
window.addEventListener('resize', function () { if (hop.target === 'wait') { hop.sky = null; hopStart(); } });
|
|
7677
|
+
})();
|
|
7678
|
+
|
|
7098
7679
|
/* ── the hub's three doors ── */
|
|
7099
7680
|
function createdRecord() {
|
|
7100
7681
|
try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) { return null; }
|