feat: /secret — the grimoire
An unlisted page at /secret. Nothing in the app links to it, it has no nav tab, and MobileNav's route-parity test now names it as an explicit exception so an ordinary page still fails that test. It IS crawlable and is advertised in the sitemap: "hidden" here means no button, not no index — a long, specific mechanics page is worth the search traffic. Every number on it was read out of the source or produced by RUNNING the real function. A grimoire of plausible-sounding numbers would be worse than none, because it would be believed. Four hand-rolled SVG figures (rarity by moon phase, the passage ladder, the two twist curves) rather than a charting dependency for four static pictures. The best thing in it is a trap I did not know existed until I ran the code: crossing over requires volatility strictly above 0.6, and favor shifts volatility DOWN by up to 0.12 at mint. So a hunter at maximum favor pulls spirits under the gate and locks themselves out of the largest reward in the game — 25 essence and +0.08 favor. Verified directly: a spirit at volatility 0.65 is crossable at favor 0.0, and is not at +0.5 or +1.0. "Do not maximise favor" is real, counter-intuitive, and follows from the source. English only, as agreed — written as prose outside the i18n system rather than several hundred translation keys, which keeps the en/es parity gate meaningful for the actual UI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { LandingPage } from './pages/LandingPage'
|
||||
import { MessagesPage } from './pages/MessagesPage'
|
||||
import { ProfilePage } from './pages/ProfilePage'
|
||||
import { SeancePage } from './pages/SeancePage'
|
||||
import { SecretPage } from './pages/SecretPage'
|
||||
import { ShopPage } from './pages/ShopPage'
|
||||
|
||||
function App() {
|
||||
@@ -52,6 +53,9 @@ function App() {
|
||||
<Route path="/hunters/:username" element={<HunterPage />} />
|
||||
<Route path="/messages" element={<MessagesPage />} />
|
||||
<Route path="/messages/:username" element={<MessagesPage />} />
|
||||
{/* Unlisted: nothing in the app links here, and it is deliberately
|
||||
absent from MobileNav. Reached by knowing the URL. */}
|
||||
<Route path="/secret" element={<SecretPage />} />
|
||||
<Route path="*" element={<LandingPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -101,7 +101,12 @@ describe('MobileNav', () => {
|
||||
p !== '*' && // catch-all, not a destination
|
||||
!p.includes(':') && // detail views, reached through their index
|
||||
p !== '/' && // the landing page is the way in, not a tab
|
||||
p !== '/enter', // the sign-in door, deliberately not a tab
|
||||
p !== '/enter' && // the sign-in door, deliberately not a tab
|
||||
// Unlisted BY DESIGN: /secret has no button anywhere in the app and
|
||||
// must never gain one. Listed here as an explicit exception rather
|
||||
// than left to slip through, so that adding an ordinary page still
|
||||
// fails this test.
|
||||
p !== '/secret',
|
||||
)
|
||||
|
||||
const covered = new Set(NAV_ITEMS.map((i) => i.to))
|
||||
|
||||
279
frontend/src/pages/SecretPage.css
Normal file
279
frontend/src/pages/SecretPage.css
Normal file
@@ -0,0 +1,279 @@
|
||||
/* The Secret — a long-form reading page.
|
||||
*
|
||||
* Deliberately quieter than the rest of the app: this is the one screen where
|
||||
* someone reads for ten minutes straight, so it drops the CRT theatrics in
|
||||
* favour of a measured column, real line-height, and figures that can breathe.
|
||||
* It still belongs to the same world — same phosphor and violet — but it reads
|
||||
* like a document rather than an instrument.
|
||||
*/
|
||||
|
||||
.secret-page {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding: 2rem 1.1rem 5rem;
|
||||
background: radial-gradient(circle at 50% 0%, #12101c 0%, #07070c 60%);
|
||||
color: #d8d4e4;
|
||||
}
|
||||
|
||||
.secret-inner {
|
||||
max-width: 46rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.secret-page h1 {
|
||||
margin: 0.2rem 0 0.8rem;
|
||||
font-size: clamp(2rem, 8vw, 3.2rem);
|
||||
letter-spacing: 0.02em;
|
||||
color: #f2eaff;
|
||||
}
|
||||
|
||||
.secret-page h2 {
|
||||
margin: 3rem 0 0.9rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid rgba(178, 107, 255, 0.28);
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: #b26bff;
|
||||
}
|
||||
|
||||
.secret-page p {
|
||||
margin: 0 0 1rem;
|
||||
/* Long-form: generous measure and leading, not the app's dense HUD spacing. */
|
||||
line-height: 1.72;
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.secret-eyebrow {
|
||||
margin: 0;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(124, 255, 178, 0.65);
|
||||
}
|
||||
|
||||
.secret-lede {
|
||||
font-size: 1.12rem !important;
|
||||
color: #efe9fb;
|
||||
}
|
||||
|
||||
.secret-warn {
|
||||
padding: 0.8rem 1rem;
|
||||
border-left: 2px solid rgba(178, 107, 255, 0.6);
|
||||
background: rgba(30, 18, 46, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.secret-danger {
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px solid rgba(255, 120, 120, 0.4);
|
||||
border-radius: 8px;
|
||||
background: rgba(46, 16, 20, 0.45);
|
||||
}
|
||||
|
||||
.secret-note {
|
||||
padding-left: 0.9rem;
|
||||
border-left: 2px solid rgba(124, 255, 178, 0.35);
|
||||
color: #b9b4c8;
|
||||
font-size: 0.96rem !important;
|
||||
}
|
||||
|
||||
/* ---- contents ---- */
|
||||
|
||||
.secret-toc {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 0.3rem;
|
||||
margin: 2rem 0 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(124, 255, 178, 0.2);
|
||||
border-radius: 10px;
|
||||
background: rgba(12, 12, 20, 0.6);
|
||||
}
|
||||
|
||||
.secret-toc a {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 6px;
|
||||
color: #7cffb2;
|
||||
text-decoration: none;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.secret-toc a:hover,
|
||||
.secret-toc a:focus-visible {
|
||||
background: rgba(124, 255, 178, 0.1);
|
||||
}
|
||||
|
||||
/* ---- traits ---- */
|
||||
|
||||
.secret-traits {
|
||||
margin: 0 0 1.2rem;
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.secret-traits > div {
|
||||
padding: 0.75rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(18, 18, 28, 0.66);
|
||||
}
|
||||
|
||||
.secret-traits dt {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: #7cffb2;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.secret-traits dd {
|
||||
margin: 0.3rem 0 0;
|
||||
line-height: 1.6;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
/* ---- code + tables ---- */
|
||||
|
||||
.secret-code {
|
||||
margin: 0 0 1.1rem;
|
||||
padding: 0.85rem 1rem;
|
||||
/* The only element here that can exceed the column, so it scrolls itself
|
||||
* rather than making the page scroll sideways. */
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(124, 255, 178, 0.22);
|
||||
border-radius: 8px;
|
||||
background: rgba(6, 10, 8, 0.8);
|
||||
color: #9dffcb;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.7;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.secret-page code {
|
||||
padding: 0.05em 0.35em;
|
||||
border-radius: 4px;
|
||||
background: rgba(124, 255, 178, 0.1);
|
||||
color: #9dffcb;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.secret-table-scroll {
|
||||
margin: 0 0 1.2rem;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border: 1px solid rgba(178, 107, 255, 0.22);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.secret-table {
|
||||
width: 100%;
|
||||
min-width: 26rem;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.secret-table th,
|
||||
.secret-table td {
|
||||
padding: 0.6rem 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.secret-table th {
|
||||
color: #b26bff;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.secret-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ---- figures ---- */
|
||||
|
||||
.secret-figure {
|
||||
margin: 0 0 1.4rem;
|
||||
padding: 0.9rem;
|
||||
border: 1px solid rgba(124, 255, 178, 0.18);
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 10, 16, 0.7);
|
||||
}
|
||||
|
||||
.secret-figure svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.secret-figure figcaption {
|
||||
margin-top: 0.6rem;
|
||||
color: #a49fb6;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.secret-axis {
|
||||
stroke: rgba(255, 255, 255, 0.25);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.secret-tick {
|
||||
fill: #8f8aa3;
|
||||
font-size: 9px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.secret-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.secret-swatch {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ---- tricks ---- */
|
||||
|
||||
.secret-tricks {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.secret-tricks li {
|
||||
line-height: 1.68;
|
||||
padding-left: 0.3rem;
|
||||
}
|
||||
|
||||
.secret-tricks b {
|
||||
color: #f0e9ff;
|
||||
}
|
||||
|
||||
.secret-foot {
|
||||
margin-top: 3.5rem;
|
||||
padding-top: 1.2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #8f8aa3;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.secret-page {
|
||||
padding-left: max(0.9rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.9rem, env(safe-area-inset-right));
|
||||
}
|
||||
.secret-toc {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
674
frontend/src/pages/SecretPage.tsx
Normal file
674
frontend/src/pages/SecretPage.tsx
Normal file
@@ -0,0 +1,674 @@
|
||||
// The Secret — /secret
|
||||
//
|
||||
// A hidden page. Nothing in the app links to it; you reach it by knowing the
|
||||
// URL. It is left crawlable on purpose (see the sitemap), because a long,
|
||||
// specific mechanics page is worth real search traffic, but it has no button
|
||||
// and no nav tab.
|
||||
//
|
||||
// EVERY NUMBER ON THIS PAGE IS READ OUT OF THE SOURCE. Where a figure is
|
||||
// derived, it was computed by running the real function rather than by
|
||||
// re-deriving the formula by hand — a grimoire of plausible-sounding numbers
|
||||
// would be worse than no grimoire, because it would be believed.
|
||||
//
|
||||
// English only, deliberately: the prose is written outside the i18n system
|
||||
// rather than as several hundred translation keys. That keeps the voice sharp
|
||||
// and keeps the en/es parity gate meaningful for the actual UI. If a Spanish
|
||||
// grimoire is ever wanted it should be its own page with its own voice, not a
|
||||
// machine translation of this one.
|
||||
|
||||
import { usePageMeta } from '../lib/pageMeta'
|
||||
import './SecretPage.css'
|
||||
|
||||
// ---- data, lifted verbatim from the backend ------------------------------
|
||||
|
||||
/** app/passage.py: LAYERS + LAYER_ESSENCE. Sums to 53. */
|
||||
const LAYERS = [
|
||||
{ id: 'listen', essence: 3, trait: 'volatility', prompt: 'how steady is it?' },
|
||||
{ id: 'name', essence: 5, trait: 'deceptiveness', prompt: 'does it answer straight?' },
|
||||
{ id: 'unbind', essence: 8, trait: 'power', prompt: 'how hard does the tether pull?' },
|
||||
{ id: 'open', essence: 12, trait: 'alignment', prompt: 'what wants to walk through?' },
|
||||
{ id: 'release', essence: 25, trait: '—', prompt: 'does it go?' },
|
||||
]
|
||||
|
||||
/** app/entities.py: rarity_weights_for_moon, normalised to percentages by
|
||||
* running the real function at illumination 0.0 / 0.5 / 1.0. */
|
||||
const RARITY = {
|
||||
tiers: ['common', 'uncommon', 'rare', 'mythic'],
|
||||
new: [55.0, 30.0, 12.0, 3.0],
|
||||
half: [40.17, 31.73, 21.76, 6.35],
|
||||
full: [25.11, 33.49, 31.66, 9.74],
|
||||
}
|
||||
|
||||
/** app/passage.py: _twist_chance sampled every 0.05. */
|
||||
const STEPS = Array.from({ length: 21 }, (_, i) => i * 0.05)
|
||||
const COLLAPSE = STEPS.map((v) => (v <= 0.5 ? 0 : 0.45 * ((v - 0.5) / 0.5)))
|
||||
const DECEIT = STEPS.map((v) => (v <= 0.55 ? 0 : 0.4 * ((v - 0.55) / 0.45)))
|
||||
|
||||
const VERDICTS = [
|
||||
{ v: 'trust', right: 'alignment ≥ 0.5', essence: 12, favor: '+0.05', wrong: '−0.10' },
|
||||
{ v: 'banish', right: 'alignment < 0.5', essence: 12, favor: '+0.05', wrong: '−0.05' },
|
||||
{ v: 'cross_over', right: 'benevolent AND volatility > 0.6', essence: 25, favor: '+0.08', wrong: '0.00' },
|
||||
{ v: 'test', right: 'a diagnostic pulse', essence: 0, favor: '0.00', wrong: '0.00' },
|
||||
]
|
||||
|
||||
const LIMITS = [
|
||||
['summon', '4 / 60s', '8 / 60s'],
|
||||
['question', '6 / 60s', '12 / 60s'],
|
||||
['ritual', '6 / 60s', '12 / 60s'],
|
||||
['judgment', '10 / 60s', '20 / 60s'],
|
||||
['passage', '20 / 60s', '40 / 60s'],
|
||||
['fragment', '30 / 60s', '60 / 60s'],
|
||||
['scry (camera)', '4 / 60s', '8 / 60s'],
|
||||
]
|
||||
|
||||
const DROPS = [
|
||||
['a correct judgment', '20%', 'trinket'],
|
||||
['a successful ritual', '25%', 'relic'],
|
||||
['summoning a rare', '15%', 'curio'],
|
||||
['summoning a mythic', '35%', 'curio'],
|
||||
]
|
||||
|
||||
// ---- chart primitives ----------------------------------------------------
|
||||
//
|
||||
// Hand-rolled SVG rather than a charting library: these are four small static
|
||||
// figures, and a dependency would cost more bundle than the whole page.
|
||||
|
||||
function LineChart({
|
||||
series,
|
||||
caption,
|
||||
}: {
|
||||
series: { points: number[]; label: string; color: string }[]
|
||||
caption: string
|
||||
}) {
|
||||
const W = 320
|
||||
const H = 150
|
||||
const PAD = 28
|
||||
const x = (i: number) => PAD + (i / (STEPS.length - 1)) * (W - PAD * 2)
|
||||
const y = (v: number) => H - PAD - (v / 0.5) * (H - PAD * 2)
|
||||
|
||||
return (
|
||||
<figure className="secret-figure">
|
||||
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={caption}>
|
||||
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} className="secret-axis" />
|
||||
<line x1={PAD} y1={PAD} x2={PAD} y2={H - PAD} className="secret-axis" />
|
||||
{series.map((s) => (
|
||||
<polyline
|
||||
key={s.label}
|
||||
fill="none"
|
||||
stroke={s.color}
|
||||
strokeWidth="2"
|
||||
points={s.points.map((v, i) => `${x(i)},${y(v)}`).join(' ')}
|
||||
/>
|
||||
))}
|
||||
<text x={PAD - 6} y={y(0.5) + 4} className="secret-tick" textAnchor="end">
|
||||
50%
|
||||
</text>
|
||||
<text x={PAD - 6} y={y(0) + 4} className="secret-tick" textAnchor="end">
|
||||
0
|
||||
</text>
|
||||
<text x={PAD} y={H - 8} className="secret-tick">
|
||||
0.0
|
||||
</text>
|
||||
<text x={W - PAD} y={H - 8} className="secret-tick" textAnchor="end">
|
||||
1.0
|
||||
</text>
|
||||
</svg>
|
||||
<figcaption>
|
||||
{caption}
|
||||
<span className="secret-legend">
|
||||
{series.map((s) => (
|
||||
<span key={s.label}>
|
||||
<span className="secret-swatch" style={{ background: s.color }} aria-hidden /> {s.label}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
function RarityChart() {
|
||||
const W = 320
|
||||
const H = 170
|
||||
const PAD = 30
|
||||
const groups = RARITY.tiers.length
|
||||
const bandW = (W - PAD * 2) / groups
|
||||
const rows = [
|
||||
{ key: 'new', label: 'new moon', color: '#4a6b8a', data: RARITY.new },
|
||||
{ key: 'half', label: 'half', color: '#7cffb2', data: RARITY.half },
|
||||
{ key: 'full', label: 'full moon', color: '#b26bff', data: RARITY.full },
|
||||
]
|
||||
const max = 55
|
||||
const barW = bandW / (rows.length + 1)
|
||||
|
||||
return (
|
||||
<figure className="secret-figure">
|
||||
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label="rarity distribution by moon phase">
|
||||
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} className="secret-axis" />
|
||||
{RARITY.tiers.map((tier, gi) =>
|
||||
rows.map((row, ri) => {
|
||||
const v = row.data[gi]
|
||||
const h = (v / max) * (H - PAD * 2)
|
||||
return (
|
||||
<rect
|
||||
key={`${tier}-${row.key}`}
|
||||
x={PAD + gi * bandW + ri * barW + barW / 2}
|
||||
y={H - PAD - h}
|
||||
width={barW - 1}
|
||||
height={h}
|
||||
fill={row.color}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
)}
|
||||
{RARITY.tiers.map((tier, gi) => (
|
||||
<text
|
||||
key={tier}
|
||||
x={PAD + gi * bandW + bandW / 2}
|
||||
y={H - PAD + 12}
|
||||
className="secret-tick"
|
||||
textAnchor="middle"
|
||||
>
|
||||
{tier}
|
||||
</text>
|
||||
))}
|
||||
</svg>
|
||||
<figcaption>
|
||||
Chance of each rarity tier, by how much of the moon is lit. Computed by running
|
||||
<code> rarity_weights_for_moon</code> and normalising.
|
||||
<span className="secret-legend">
|
||||
{rows.map((r) => (
|
||||
<span key={r.key}>
|
||||
<span className="secret-swatch" style={{ background: r.color }} aria-hidden /> {r.label}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
function LadderChart() {
|
||||
const W = 320
|
||||
const H = 150
|
||||
const PAD = 30
|
||||
const max = 25
|
||||
const bandW = (W - PAD * 2) / LAYERS.length
|
||||
|
||||
return (
|
||||
<figure className="secret-figure">
|
||||
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label="essence paid by each passage layer">
|
||||
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} className="secret-axis" />
|
||||
{LAYERS.map((l, i) => {
|
||||
const h = (l.essence / max) * (H - PAD * 2)
|
||||
return (
|
||||
<g key={l.id}>
|
||||
<rect
|
||||
x={PAD + i * bandW + 4}
|
||||
y={H - PAD - h}
|
||||
width={bandW - 8}
|
||||
height={h}
|
||||
fill={l.id === 'release' ? '#b26bff' : '#7cffb2'}
|
||||
/>
|
||||
<text
|
||||
x={PAD + i * bandW + bandW / 2}
|
||||
y={H - PAD - h - 4}
|
||||
className="secret-tick"
|
||||
textAnchor="middle"
|
||||
>
|
||||
{l.essence}
|
||||
</text>
|
||||
<text
|
||||
x={PAD + i * bandW + bandW / 2}
|
||||
y={H - PAD + 12}
|
||||
className="secret-tick"
|
||||
textAnchor="middle"
|
||||
>
|
||||
{l.id}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
<figcaption>Essence paid by each beat of the Passage. The five sum to 53.</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- the page ------------------------------------------------------------
|
||||
|
||||
export function SecretPage() {
|
||||
usePageMeta({
|
||||
title: 'The Secret — Quantumancy',
|
||||
description:
|
||||
'The complete mechanics of Quantumancy: entity traits, the moon and rarity, the veil, the ritual, judgment, the Passage, favor, entropy, and the tricks only the source reveals.',
|
||||
})
|
||||
|
||||
return (
|
||||
<main className="secret-page">
|
||||
<div className="secret-inner">
|
||||
<header className="secret-head">
|
||||
<p className="secret-eyebrow">unlisted</p>
|
||||
<h1>The Secret</h1>
|
||||
<p className="secret-lede">
|
||||
Nothing links here. There is no button, and there never will be. What follows is
|
||||
every mechanism the instrument actually runs on, with the real constants, read
|
||||
straight out of the source rather than remembered.
|
||||
</p>
|
||||
<p className="secret-warn">
|
||||
Read it once and the séance stops being weather and starts being a system you can
|
||||
work. Some people would rather not know. This is your last exit.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav className="secret-toc" aria-label="contents">
|
||||
<a href="#truth">I. The hidden truth</a>
|
||||
<a href="#moon">II. The moon</a>
|
||||
<a href="#veil">III. The veil</a>
|
||||
<a href="#ritual">IV. The ritual</a>
|
||||
<a href="#judgment">V. Judgment</a>
|
||||
<a href="#passage">VI. The Passage</a>
|
||||
<a href="#favor">VII. Favor, and its trap</a>
|
||||
<a href="#entropy">VIII. Why you cannot cheat</a>
|
||||
<a href="#economy">IX. The economy</a>
|
||||
<a href="#tricks">X. The tricks</a>
|
||||
</nav>
|
||||
|
||||
<section id="truth">
|
||||
<h2>I. The hidden truth</h2>
|
||||
<p>
|
||||
Every spirit carries four numbers, each between 0 and 1. They are decided the
|
||||
moment it is minted and they never change. Everything else in the game is a
|
||||
question about these four, asked in a different way.
|
||||
</p>
|
||||
<dl className="secret-traits">
|
||||
<div>
|
||||
<dt>alignment</dt>
|
||||
<dd>
|
||||
whether it means harm. At or above <b>0.5</b> it is benevolent. This is the
|
||||
single number a <code>trust</code> or <code>banish</code> verdict is graded
|
||||
against.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>power</dt>
|
||||
<dd>how strongly it is tethered here. Makes the ritual harder.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>volatility</dt>
|
||||
<dd>
|
||||
how unstable it is. Above <b>0.6</b> it is <i>stuck</i> — and being stuck is
|
||||
the precondition for crossing over. It also drives the Passage collapse.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>deceptiveness</dt>
|
||||
<dd>
|
||||
how much it lies. Above <b>0.55</b> it can lie to you during the Passage, and
|
||||
it makes the ritual harder.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="secret-note">
|
||||
There is no second, secret set of stats. The ritual reveals these, the Passage
|
||||
reveals these, and judgment grades against these. When a spirit lies, it does not
|
||||
invent a number — it <b>mirrors</b> the real one: a hidden 0.8 is shown as 0.2. A
|
||||
lie is therefore exactly recoverable once you know it was a lie.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="moon">
|
||||
<h2>II. The moon</h2>
|
||||
<p>
|
||||
The app computes the true lunar phase from the synodic month (29.530588853 days)
|
||||
against a known new moon, not a calendar approximation. Illumination then skews
|
||||
the rarity table. This is the single largest controllable effect in the game and
|
||||
it costs you nothing but patience.
|
||||
</p>
|
||||
<RarityChart />
|
||||
<p>
|
||||
At new moon a mythic is a <b>3%</b> lottery. At full moon it is <b>9.74%</b> —
|
||||
more than three times as likely. Rare goes <b>12% → 31.66%</b>. Together, rare or
|
||||
better goes from 15% to <b>41.4%</b> of everything you summon.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
It is a skew, never a gate. Every tier stays reachable on every night, by
|
||||
deliberate design — a seeker who can only play on a Tuesday is never locked out.
|
||||
But if you are choosing a night, choose a full one.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="veil">
|
||||
<h2>III. The veil</h2>
|
||||
<p>
|
||||
Call a channel you have called before and the same presence usually answers.
|
||||
"Usually" is <b>72%</b>, and it is reduced by how thin the veil is:
|
||||
</p>
|
||||
<pre className="secret-code">
|
||||
return_chance = 0.72 × (1 − thinness × 0.45)
|
||||
</pre>
|
||||
<p>
|
||||
Thinness rises toward solar midnight — real solar midnight for your longitude, if
|
||||
you granted location, not local clock midnight — and with the moon. If the
|
||||
geomagnetic feed is live, the final figure is blended with real space weather:
|
||||
</p>
|
||||
<pre className="secret-code">thinness = thinness × 0.67 + geomagnetic_disturbance × 0.33</pre>
|
||||
<p>
|
||||
That disturbance is the NOAA planetary K-index, mapped from its 0–9 scale onto
|
||||
0–1. A K of 5 or above is an official geomagnetic storm.
|
||||
</p>
|
||||
<p>
|
||||
So a thin veil is <b>double-edged</b>, and this is the part most people get
|
||||
backwards. A full moon at solar midnight during a storm drags the return chance
|
||||
from 72% down to about <b>40%</b>. Good for meeting someone new. Bad for finding
|
||||
someone again.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="ritual">
|
||||
<h2>IV. The ritual</h2>
|
||||
<p>
|
||||
Four steps, then one roll. Only <code>power</code> and <code>deceptiveness</code>{' '}
|
||||
matter — a strong, cagey spirit is hardest to read. Volatility deliberately does
|
||||
not count here.
|
||||
</p>
|
||||
<pre className="secret-code">
|
||||
difficulty = (power + deceptiveness) / 2{'\n'}
|
||||
chance = 0.65 − 0.35 × difficulty{'\n'}
|
||||
floor = 0.30
|
||||
</pre>
|
||||
<p>
|
||||
So the kindest spirit gives you <b>65%</b> and the worst possible one still gives
|
||||
you <b>30%</b>. There is no unwinnable entity. Success reveals all four true
|
||||
traits — the whole hidden truth, handed over.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
Failing costs you nothing but the attempt, and you may attempt again. The milestone
|
||||
pays <b>once per spirit</b> either way, so re-attempting after a success is
|
||||
pointless for essence and useful only if you somehow missed the reveal.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="judgment">
|
||||
<h2>V. Judgment</h2>
|
||||
<div className="secret-table-scroll">
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>verdict</th>
|
||||
<th>correct when</th>
|
||||
<th>essence</th>
|
||||
<th>favor</th>
|
||||
<th>if wrong</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{VERDICTS.map((v) => (
|
||||
<tr key={v.v}>
|
||||
<td>
|
||||
<code>{v.v}</code>
|
||||
</td>
|
||||
<td>{v.right}</td>
|
||||
<td>{v.essence}</td>
|
||||
<td>{v.favor}</td>
|
||||
<td>{v.wrong}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
Note the asymmetry: a wrong <code>trust</code> costs <b>−0.10</b> favor, twice the
|
||||
wrong <code>banish</code>. Trusting something malevolent is the reckless failure;
|
||||
turning away something harmless is merely over-cautious. And a failed{' '}
|
||||
<code>cross_over</code> costs <b>nothing</b> — it is read as a naive act of mercy,
|
||||
not a blunder. Guessing cross_over is free.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
<code>test</code> never touches the ledger, which is why it is the only verdict you
|
||||
may repeat forever.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="passage">
|
||||
<h2>VI. The Passage</h2>
|
||||
<p>
|
||||
Five beats. Each opens one real trait, pays essence, and can go wrong.
|
||||
</p>
|
||||
<LadderChart />
|
||||
<div className="secret-table-scroll">
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>beat</th>
|
||||
<th>reveals</th>
|
||||
<th>essence</th>
|
||||
<th>asks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LAYERS.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td>
|
||||
<code>{l.id}</code>
|
||||
</td>
|
||||
<td>{l.trait}</td>
|
||||
<td>{l.essence}</td>
|
||||
<td>{l.prompt}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
Two things can go wrong, and both are linear ramps off a floor — below the floor
|
||||
they are impossible, not merely unlikely.
|
||||
</p>
|
||||
<LineChart
|
||||
caption="Twist chance against the underlying trait."
|
||||
series={[
|
||||
{ points: COLLAPSE, label: 'collapse (volatility)', color: '#b26bff' },
|
||||
{ points: DECEIT, label: 'lie (deceptiveness)', color: '#7cffb2' },
|
||||
]}
|
||||
/>
|
||||
<p>
|
||||
<b>Collapse</b> is zero at or below volatility <b>0.5</b>, rising to <b>45%</b> at
|
||||
1.0. It sends the rite back to <code>listen</code> — but your essence stays. It can
|
||||
only strike <code>name</code>, <code>unbind</code> and <code>open</code>.
|
||||
</p>
|
||||
<p>
|
||||
<b>Lying</b> is zero at or below deceptiveness <b>0.55</b>, rising to <b>40%</b>.
|
||||
The lie is not marked when it happens. It is struck through one beat later, when
|
||||
the rite catches it. If you are reading carefully, a reveal that contradicts what
|
||||
the ritual already told you is a lie you can invert on the spot.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
<code>listen</code> and <code>release</code> cannot collapse, and the reason is
|
||||
load-bearing rather than cosmetic: crossing requires volatility above 0.6, so every
|
||||
spirit that <i>can</i> cross is exactly a spirit with a high collapse chance. If
|
||||
the final beat could collapse, the most crossable spirits would be the ones you
|
||||
could least often finish. That would be backwards, so it is forbidden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="favor">
|
||||
<h2>VII. Favor, and its trap</h2>
|
||||
<p>
|
||||
Favor runs −1.0 to +1.0 and it is not a score. It is a lens. When a new spirit is
|
||||
minted, your favor bends its two <i>legibility</i> traits:
|
||||
</p>
|
||||
<pre className="secret-code">
|
||||
shift = −0.12 × favor{'\n'}
|
||||
volatility += shift{'\n'}
|
||||
deceptiveness += shift
|
||||
</pre>
|
||||
<p>
|
||||
High favor means calmer, more honest spirits: less collapse, fewer lies, easier
|
||||
rituals. Alignment and power are untouched — favor changes how readable a spirit
|
||||
is, never who it is.
|
||||
</p>
|
||||
<p className="secret-danger">
|
||||
<b>And here is the trap, which almost nobody will find.</b> Crossing over requires
|
||||
volatility strictly above <b>0.6</b>. At favor +1.0 every spirit you mint has its
|
||||
volatility pulled down by 0.12. A spirit that would have rolled 0.65 — comfortably
|
||||
crossable — arrives at <b>0.53</b>, and can no longer be crossed at all. You have
|
||||
made the dead calmer, and in doing so you have locked yourself out of the largest
|
||||
reward in the game: 25 essence and +0.08 favor, the most compassionate act
|
||||
available to you.
|
||||
</p>
|
||||
<p>
|
||||
Verified by running the real functions: at favor 0.0 that spirit is crossable; at
|
||||
+0.5 it is not; at +1.0 it is not. If you are farming crossings, <b>do not</b>{' '}
|
||||
maximise favor. Sit nearer the middle.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="entropy">
|
||||
<h2>VIII. Why you cannot cheat</h2>
|
||||
<p>
|
||||
The randomness is not <code>Math.random()</code>. Your device harvests physical
|
||||
noise — the microphone's noise floor, RF hiss between stations — and that sample
|
||||
is Von Neumann debiased, then conditioned through SHA-256 before it ever leaves the
|
||||
page.
|
||||
</p>
|
||||
<p>
|
||||
On the server it is mixed by HMAC with a secret generated fresh at boot, and each
|
||||
draw is domain-separated by a context string, so the collapse draw and the lie draw
|
||||
on the same beat are mathematically unrelated.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
The consequence is precise and worth understanding: a client can only ever <b>add</b>{' '}
|
||||
unpredictability. Sending a chosen or replayed entropy value cannot steer any
|
||||
outcome, because your contribution is never a seed on its own — it is one input to
|
||||
a keyed hash whose key you do not have. The room genuinely participates. It just
|
||||
cannot be told what to say.
|
||||
</p>
|
||||
<p>
|
||||
The same idea drives the spirits' words: the language model is given a fixed{' '}
|
||||
<code>seed</code>, which pins its sampling — so the noise in your room, not the
|
||||
model's own dice, selects which words come out.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="economy">
|
||||
<h2>IX. The economy</h2>
|
||||
<p>
|
||||
Every essence source, exhaustively: <b>2</b> per summon, <b>15</b> for a completed
|
||||
ritual, <b>12</b> for a correct trust or banish, <b>25</b> for a correct cross_over,
|
||||
and <b>3/5/8/12/25</b> for the Passage beats.
|
||||
</p>
|
||||
<div className="secret-table-scroll">
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>drops after</th>
|
||||
<th>chance</th>
|
||||
<th>kind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{DROPS.map((d) => (
|
||||
<tr key={d[0]}>
|
||||
<td>{d[0]}</td>
|
||||
<td>{d[1]}</td>
|
||||
<td>{d[2]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
Note that a mythic summon drops at <b>35%</b> — the highest rate in the game, and
|
||||
another reason the full moon matters.
|
||||
</p>
|
||||
<p className="secret-note">
|
||||
Every milestone pays <b>once per spirit, permanently</b>. Not once per session, not
|
||||
once per connection: there is a durable ledger, so re-walking a rite, reconnecting,
|
||||
or opening a second tab pays nothing further. Re-walking still reveals — it just
|
||||
does not mint. The only way to earn again is a genuinely new presence.
|
||||
</p>
|
||||
<div className="secret-table-scroll">
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>action</th>
|
||||
<th>per account</th>
|
||||
<th>per address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LIMITS.map((l) => (
|
||||
<tr key={l[0]}>
|
||||
<td>{l[0]}</td>
|
||||
<td>{l[1]}</td>
|
||||
<td>{l[2]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tricks">
|
||||
<h2>X. The tricks</h2>
|
||||
<ol className="secret-tricks">
|
||||
<li>
|
||||
<b>Hunt on a full moon.</b> Rare-or-better goes from 15% to 41.4% of summons. It
|
||||
is the largest lever you have and it is free.
|
||||
</li>
|
||||
<li>
|
||||
<b>Do not max your favor if you want crossings.</b> Above roughly +0.4 you start
|
||||
pulling spirits under the 0.6 volatility gate and making them uncrossable. This
|
||||
is the single most counter-intuitive fact in the game.
|
||||
</li>
|
||||
<li>
|
||||
<b>Guessing cross_over is free.</b> A failed cross_over costs zero favor. If a
|
||||
spirit reads benevolent and unstable, always try it before settling for trust.
|
||||
</li>
|
||||
<li>
|
||||
<b>Run the ritual before judging.</b> Success hands you all four true numbers,
|
||||
which turns every subsequent verdict from a read into a lookup.
|
||||
</li>
|
||||
<li>
|
||||
<b>A thin veil is for meeting, not returning.</b> Solar midnight, full moon and a
|
||||
geomagnetic storm cut your chance of reaching a known spirit again by nearly half.
|
||||
Do your reunions in daylight on a quiet sky.
|
||||
</li>
|
||||
<li>
|
||||
<b>Catch the lie by contradiction.</b> Lies are mirrored, not random. If a
|
||||
Passage reveal disagrees with what the ritual already showed you, invert it and
|
||||
you have the truth exactly.
|
||||
</li>
|
||||
<li>
|
||||
<b>A spirit under 0.55 deceptiveness cannot lie to you at all.</b> Not "rarely" —
|
||||
the chance is exactly zero. The same is true of collapse under 0.5 volatility.
|
||||
Once the ritual shows you those numbers, you know precisely which risks are off
|
||||
the table.
|
||||
</li>
|
||||
<li>
|
||||
<b>Wrong banish is cheaper than wrong trust.</b> Half the penalty. When you are
|
||||
genuinely torn and the stakes are favor, err toward caution.
|
||||
</li>
|
||||
<li>
|
||||
<b>Grant location.</b> Without it, solar midnight cannot be computed for you and
|
||||
you lose that lever on the veil entirely.
|
||||
</li>
|
||||
<li>
|
||||
<b>Feed the room.</b> The microphone and the sensors are not decoration — they
|
||||
are the actual entropy source. A silent, shielded room gives the summoning less
|
||||
to work with.
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<footer className="secret-foot">
|
||||
<p>
|
||||
Every figure here was read from the source or produced by running the real
|
||||
function. Where the code changes, this page will be wrong before anyone notices —
|
||||
so trust the séance over the grimoire if they ever disagree.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user