@meycult/core 0.1.0 → 0.2.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/package.json +3 -6
- package/src/brand/BrandName.tsx +4 -2
- package/src/brand/LaunchButton.tsx +5 -3
- package/src/brand/Logo.tsx +25 -6
- package/src/brand/PyramidName.tsx +10 -0
- package/src/brand/Sigil.tsx +10 -5
- package/src/brand/TokenIcons.tsx +12 -6
- package/src/brand.ts +4 -1
- package/src/components/CultCountdown.tsx +89 -14
- package/src/game/categories.ts +23 -0
- package/src/game/cults.ts +88 -0
- package/src/game/quests.ts +62 -0
- package/src/game/types.ts +27 -0
- package/src/index.ts +64 -21
- package/src/product.ts +17 -0
- package/src/styles/theme.css +97 -6
- package/src/tokens.ts +10 -2
- package/src/ui/CultButton.tsx +55 -0
- package/src/ui/RarityBadge.tsx +18 -0
- package/src/ui/animated-shiny-text.tsx +2 -2
- package/src/ui/badges.tsx +43 -0
- package/src/ui/card.tsx +20 -92
- package/src/ui/shimmer-button.tsx +5 -3
- package/src/ui/virtues.tsx +66 -0
- package/src/unveil/LaunchStage.tsx +3 -2
- package/src/unveil/bg.ts +20 -11
- package/src/unveil/bgStyles.ts +0 -1
- package/src/unveil/blocks.tsx +51 -14
- package/src/unveil/effects.tsx +182 -86
- package/src/unveil.ts +2 -21
- package/src/auth/Gate.tsx +0 -14
- package/src/auth/types.ts +0 -12
- package/src/brand/GodEmperor.tsx +0 -8
- package/src/cn.ts +0 -6
- package/src/lib/images.ts +0 -10
- package/src/lib/money.ts +0 -12
- package/src/lib/store.ts +0 -19
- package/src/shell/types.ts +0 -13
- package/src/supabase/browser.ts +0 -103
- package/src/supabase/callback.ts +0 -15
- package/src/supabase/proxy.ts +0 -36
- package/src/supabase/server.ts +0 -31
- package/src/ui/Callout.tsx +0 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meycult/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MeyCult shared core — yellow theme, brand, supabase/auth factories, lib. Source-only, no build.",
|
|
6
6
|
"license": "ISC",
|
|
@@ -24,11 +24,8 @@
|
|
|
24
24
|
"exports": {
|
|
25
25
|
".": "./src/index.ts",
|
|
26
26
|
"./tokens": "./src/tokens.ts",
|
|
27
|
-
"./cn": "./src/cn.ts",
|
|
28
27
|
"./brand": "./src/brand.ts",
|
|
29
|
-
"./
|
|
30
|
-
"./supabase/proxy": "./src/supabase/proxy.ts",
|
|
31
|
-
"./supabase/callback": "./src/supabase/callback.ts",
|
|
28
|
+
"./product": "./src/product.ts",
|
|
32
29
|
"./styles/theme.css": "./src/styles/theme.css"
|
|
33
30
|
},
|
|
34
31
|
"scripts": {
|
|
@@ -36,7 +33,7 @@
|
|
|
36
33
|
"release": "bash scripts/release.sh"
|
|
37
34
|
},
|
|
38
35
|
"dependencies": {
|
|
39
|
-
"@meyvoid/core": "^0.
|
|
36
|
+
"@meyvoid/core": "^0.2.0",
|
|
40
37
|
"clsx": "^2.1.1",
|
|
41
38
|
"tailwind-merge": "^3.6.0",
|
|
42
39
|
"zustand": "^5.0.14"
|
package/src/brand/BrandName.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { GLOW } from '../tokens'
|
|
2
|
+
|
|
1
3
|
export default function BrandName({ className }: { className?: string }) {
|
|
2
4
|
return (
|
|
3
5
|
<span className={className} style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, textTransform: 'none' }}>
|
|
4
6
|
<span className="brand-mey" style={{
|
|
5
|
-
textShadow:
|
|
7
|
+
textShadow: GLOW.brandMey,
|
|
6
8
|
}}>Mey</span>
|
|
7
9
|
<span className="brand-cult" style={{
|
|
8
|
-
textShadow:
|
|
10
|
+
textShadow: GLOW.brandCult,
|
|
9
11
|
}}>Cult</span>
|
|
10
12
|
</span>
|
|
11
13
|
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { GRADIENTS, GLOW } from '../tokens'
|
|
2
|
+
|
|
1
3
|
interface LaunchButtonProps {
|
|
2
4
|
label?: string
|
|
3
5
|
href?: string
|
|
@@ -8,14 +10,14 @@ interface LaunchButtonProps {
|
|
|
8
10
|
export default function LaunchButton({ label = 'Launch', href = 'https://app.meycult.com', className = '', small }: LaunchButtonProps) {
|
|
9
11
|
return (
|
|
10
12
|
<span className={`inline-block rounded-lg p-[2px] hover:scale-[1.02] transition-all ${className}`}
|
|
11
|
-
style={{ background:
|
|
13
|
+
style={{ background: GRADIENTS.launch }}>
|
|
12
14
|
<a
|
|
13
15
|
href={href}
|
|
14
16
|
className={`block font-bold tracking-wider rounded-[6px] hover:opacity-90 transition-all whitespace-nowrap ${small ? 'px-3 py-1.5 text-[10px]' : 'px-8 py-2.5 text-sm'}`}
|
|
15
17
|
style={{
|
|
16
|
-
background:
|
|
18
|
+
background: GRADIENTS.launchBody,
|
|
17
19
|
color: '#ffffff',
|
|
18
|
-
boxShadow:
|
|
20
|
+
boxShadow: GLOW.text,
|
|
19
21
|
}}
|
|
20
22
|
>
|
|
21
23
|
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700 }}>{label} </span>
|
package/src/brand/Logo.tsx
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
|
+
import type { LinkComponent } from '@meyvoid/core'
|
|
2
|
+
import { BRAND } from '../brand'
|
|
1
3
|
import Sigil from './Sigil'
|
|
2
4
|
|
|
3
|
-
export default function Logo({
|
|
5
|
+
export default function Logo({
|
|
6
|
+
size = 24,
|
|
7
|
+
className,
|
|
8
|
+
href = '/',
|
|
9
|
+
tagline = BRAND.tagline,
|
|
10
|
+
product,
|
|
11
|
+
Link,
|
|
12
|
+
}: {
|
|
13
|
+
size?: number
|
|
14
|
+
className?: string
|
|
15
|
+
href?: string
|
|
16
|
+
tagline?: string
|
|
17
|
+
/** Product lockup (e.g. <PyramidName/>) — replaces the tagline line when set. */
|
|
18
|
+
product?: React.ReactNode
|
|
19
|
+
/** Injectable link — host passes react-router Link (app) or next/link (web). Defaults to <a>. */
|
|
20
|
+
Link?: LinkComponent
|
|
21
|
+
}) {
|
|
22
|
+
const Cmp = (Link ?? 'a') as LinkComponent
|
|
4
23
|
return (
|
|
5
|
-
<
|
|
24
|
+
<Cmp href={href} className={`flex items-center gap-3 ${className || ''}`}>
|
|
6
25
|
<Sigil size={size} />
|
|
7
26
|
<div>
|
|
8
27
|
<div
|
|
9
28
|
className="text-lg font-bold tracking-wider leading-none"
|
|
10
29
|
style={{ fontFamily: 'var(--font-logo)' }}
|
|
11
30
|
>
|
|
12
|
-
<span className="brand-mey">
|
|
13
|
-
<span className="brand-cult" style={{ textShadow: '0 0 12px rgba(255,255,255,0.5), 0 0 30px rgba(255,255,255,0.2)' }}>
|
|
31
|
+
<span className="brand-mey">{BRAND.first}</span>
|
|
32
|
+
<span className="brand-cult" style={{ textShadow: '0 0 12px rgba(255,255,255,0.5), 0 0 30px rgba(255,255,255,0.2)' }}>{BRAND.second}</span>
|
|
14
33
|
</div>
|
|
15
34
|
<div
|
|
16
35
|
className="text-[10px] uppercase tracking-widest mt-0.5"
|
|
17
36
|
style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-logo)' }}
|
|
18
37
|
>
|
|
19
|
-
|
|
38
|
+
{product ?? tagline}
|
|
20
39
|
</div>
|
|
21
40
|
</div>
|
|
22
|
-
</
|
|
41
|
+
</Cmp>
|
|
23
42
|
)
|
|
24
43
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PRODUCT } from '../product'
|
|
2
|
+
|
|
3
|
+
/** Pyramid wordmark — the game product. Pair with BrandName (MeyCult) above it. */
|
|
4
|
+
export default function PyramidName({ className }: { className?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<span className={`brand-pyramid ${className ?? ''}`}>
|
|
7
|
+
{PRODUCT.name}
|
|
8
|
+
</span>
|
|
9
|
+
)
|
|
10
|
+
}
|
package/src/brand/Sigil.tsx
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
|
+
import { cult as cultSigil } from '@meyvoid/core/sigils'
|
|
2
|
+
|
|
3
|
+
/** Cult mark — geometry is canonical in @meyvoid/core/sigils, styling (accent fill, glow) is cult. */
|
|
1
4
|
export default function Sigil({ size = 24 }: { size?: number }) {
|
|
2
5
|
const s = size * 1.5
|
|
3
6
|
|
|
4
7
|
return (
|
|
5
8
|
<svg
|
|
6
9
|
width={s}
|
|
7
|
-
viewBox=
|
|
10
|
+
viewBox={cultSigil.VIEW_BOX}
|
|
8
11
|
style={{ filter: 'drop-shadow(var(--glow))' }}
|
|
9
12
|
>
|
|
10
13
|
<g transform="translate(-341.95552,-53.816747)">
|
|
11
14
|
<g transform="translate(15.212189,33.321951)" style={{ fill: 'var(--color-accent)' }}>
|
|
12
15
|
<path
|
|
13
16
|
style={{ fill: 'var(--color-accent)', fillOpacity: 1 }}
|
|
14
|
-
d=
|
|
17
|
+
d={cultSigil.SURROUND_D}
|
|
15
18
|
/>
|
|
16
19
|
</g>
|
|
17
20
|
<path
|
|
18
21
|
style={{ fill: '#ffffff', fillOpacity: 1 }}
|
|
19
|
-
d=
|
|
22
|
+
d={cultSigil.EYE_D}
|
|
20
23
|
/>
|
|
21
|
-
<
|
|
24
|
+
<circle
|
|
25
|
+
cx={cultSigil.PUPIL.cx}
|
|
26
|
+
cy={cultSigil.PUPIL.cy}
|
|
27
|
+
r={cultSigil.PUPIL.r}
|
|
22
28
|
style={{ fill: 'var(--color-bg)' }}
|
|
23
|
-
d="m 442.49623,120.3747 c -22.51598,-4e-5 -40.76826,18.25286 -40.76822,40.76884 -10e-6,22.51597 18.25225,40.76887 40.76822,40.76883 22.51558,-5.3e-4 40.76824,-18.25326 40.76823,-40.76883 3e-5,-22.51557 -18.25265,-40.76832 -40.76823,-40.76884 z"
|
|
24
29
|
/>
|
|
25
30
|
</g>
|
|
26
31
|
</svg>
|
package/src/brand/TokenIcons.tsx
CHANGED
|
@@ -3,6 +3,8 @@ export interface TokenIconProps {
|
|
|
3
3
|
/** Asset base — host's public dir mount. Defaults to '/products'. */
|
|
4
4
|
basePath?: string
|
|
5
5
|
className?: string
|
|
6
|
+
/** Render icon only (no wordmark) — app ergonomics. */
|
|
7
|
+
icon?: boolean
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export function InfluenceIcon({ size = 14, basePath = '/products', className }: TokenIconProps) {
|
|
@@ -27,22 +29,26 @@ export function FateIcon({ size = 14, basePath = '/products', className }: Token
|
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export function InfluenceText({ size = 13, basePath = '/products' }: TokenIconProps) {
|
|
32
|
+
export function InfluenceText({ size = 13, basePath = '/products', icon = true, className }: TokenIconProps) {
|
|
31
33
|
return (
|
|
32
|
-
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }}>
|
|
33
|
-
|
|
34
|
+
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }} className={className}>
|
|
35
|
+
{icon && <><InfluenceIcon size={size} basePath={basePath} />{' '}</>}
|
|
34
36
|
<span className="brand-mey">Mey</span>
|
|
35
37
|
<span style={{ color: 'var(--color-accent)' }}>Influence</span>
|
|
36
38
|
</span>
|
|
37
39
|
)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
export function FateText({ size = 13, basePath = '/products' }: TokenIconProps) {
|
|
42
|
+
export function FateText({ size = 13, basePath = '/products', icon = true, className }: TokenIconProps) {
|
|
41
43
|
return (
|
|
42
|
-
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }}>
|
|
43
|
-
|
|
44
|
+
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }} className={className}>
|
|
45
|
+
{icon && <><FateIcon size={size} basePath={basePath} />{' '}</>}
|
|
44
46
|
<span className="brand-mey">Mey</span>
|
|
45
47
|
<span style={{ color: 'var(--color-accent)' }}>Fate</span>
|
|
46
48
|
</span>
|
|
47
49
|
)
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
/** App-ergonomic aliases. */
|
|
53
|
+
export const MeyInfluence = InfluenceText
|
|
54
|
+
export const MeyFate = FateText
|
package/src/brand.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { Brand } from '@meyvoid/core/brand'
|
|
2
|
+
|
|
1
3
|
export const BRAND = {
|
|
2
4
|
name: 'MeyCult',
|
|
3
5
|
first: 'Mey',
|
|
4
6
|
second: 'Cult',
|
|
5
7
|
tagline: 'Wager & Join',
|
|
6
8
|
description: 'Where Foresight Becomes Fortune. Gamified prediction market on Base L2.',
|
|
7
|
-
|
|
9
|
+
domain: 'meycult.com',
|
|
10
|
+
} as const satisfies Brand
|
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
* SSR-safe (skeleton until mount, no hydration mismatch), screen-reader
|
|
6
6
|
* friendly (role=timer stays silent; separate polite status per minute).
|
|
7
7
|
*/
|
|
8
|
-
import { useEffect, useState, type CSSProperties } from 'react'
|
|
8
|
+
import { useEffect, useRef, useState, type CSSProperties } from 'react'
|
|
9
9
|
import { angelRuns } from '@meyvoid/core/angel'
|
|
10
|
-
import {
|
|
10
|
+
import { diffParts, pad2, type CountdownParts } from '@meyvoid/core'
|
|
11
|
+
import { cn } from '@meyvoid/core/cn'
|
|
11
12
|
import {
|
|
12
13
|
UNVEIL_AT,
|
|
13
|
-
diffParts,
|
|
14
14
|
unveilDayLabel,
|
|
15
|
-
pad2,
|
|
16
|
-
type CountdownParts,
|
|
17
15
|
} from '../unveil'
|
|
18
16
|
|
|
19
17
|
export interface CultCountdownProps {
|
|
@@ -55,6 +53,52 @@ export function CultCountdown({
|
|
|
55
53
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
54
|
}, [day, hour, minute])
|
|
57
55
|
|
|
56
|
+
// tier holds: landing in an angel tier freezes that exact display, then
|
|
57
|
+
// the real countdown resumes — pair 2s, trio 10s, all-equal full house
|
|
58
|
+
// 60s. Tier-4 non-matching never holds (glow only). Cooldown stops
|
|
59
|
+
// slow cells (11:11 for a minute) refreezing every hold; a higher live
|
|
60
|
+
// tier mid-hold upgrades (trio never swallows a full-house second).
|
|
61
|
+
// NOTE: hooks live above the early returns — hook order must never change.
|
|
62
|
+
const HOLD_MS: Record<number, number> = { 2: 2000, 3: 10000, 4: 60000 }
|
|
63
|
+
const [frozen, setFrozen] = useState<string[] | null>(null)
|
|
64
|
+
const [frozenMask, setFrozenMask] = useState<boolean[] | null>(null)
|
|
65
|
+
const freezing = useRef(false)
|
|
66
|
+
const cooldownTier = useRef(0)
|
|
67
|
+
const holdTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
68
|
+
useEffect(() => () => {
|
|
69
|
+
if (holdTimer.current) clearTimeout(holdTimer.current)
|
|
70
|
+
}, [])
|
|
71
|
+
const startHold = (tier: number, disp: string[], mask: boolean[]) => {
|
|
72
|
+
if (holdTimer.current) clearTimeout(holdTimer.current)
|
|
73
|
+
freezing.current = true
|
|
74
|
+
setFrozen(disp)
|
|
75
|
+
setFrozenMask(mask)
|
|
76
|
+
holdTimer.current = setTimeout(() => {
|
|
77
|
+
freezing.current = false
|
|
78
|
+
cooldownTier.current = tier
|
|
79
|
+
setFrozen(null)
|
|
80
|
+
setFrozenMask(null)
|
|
81
|
+
holdTimer.current = null
|
|
82
|
+
}, HOLD_MS[tier])
|
|
83
|
+
}
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!parts || parts.done) return
|
|
86
|
+
const vals = [parts.days, parts.hours, parts.minutes, parts.seconds]
|
|
87
|
+
const disp = vals.map(pad2)
|
|
88
|
+
const mask = angelRuns(vals)
|
|
89
|
+
const liveTier = mask.filter(Boolean).length
|
|
90
|
+
const liveEqual = disp.every((d) => d === disp[0])
|
|
91
|
+
const holdable = liveTier >= 2 && (liveTier <= 3 || liveEqual)
|
|
92
|
+
if (liveTier < cooldownTier.current) cooldownTier.current = 0
|
|
93
|
+
if (freezing.current) {
|
|
94
|
+
const heldTier = (frozenMask ?? mask).filter(Boolean).length
|
|
95
|
+
if (liveTier > heldTier && holdable) startHold(liveTier, disp, mask)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (holdable && liveTier > cooldownTier.current) startHold(liveTier, disp, mask)
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [parts])
|
|
101
|
+
|
|
58
102
|
if (!parts) {
|
|
59
103
|
return (
|
|
60
104
|
<div aria-hidden="true" className={cn('animate-pulse', className)}>
|
|
@@ -71,22 +115,49 @@ export function CultCountdown({
|
|
|
71
115
|
)
|
|
72
116
|
}
|
|
73
117
|
|
|
118
|
+
const shown: string[] = frozen ?? [
|
|
119
|
+
pad2(parts.days),
|
|
120
|
+
pad2(parts.hours),
|
|
121
|
+
pad2(parts.minutes),
|
|
122
|
+
pad2(parts.seconds),
|
|
123
|
+
]
|
|
124
|
+
const angels: boolean[] = frozenMask ?? angelRuns([parts.days, parts.hours, parts.minutes, parts.seconds])
|
|
74
125
|
const cells: Array<[string, string]> = [
|
|
75
|
-
[
|
|
76
|
-
[
|
|
77
|
-
[
|
|
78
|
-
[
|
|
126
|
+
[shown[0], 'days'],
|
|
127
|
+
[shown[1], 'hrs'],
|
|
128
|
+
[shown[2], 'min'],
|
|
129
|
+
[shown[3], 'sec'],
|
|
79
130
|
]
|
|
80
|
-
// angel numbers glow yellow; everything else stays white
|
|
81
|
-
|
|
131
|
+
// angel numbers glow yellow; everything else stays white.
|
|
132
|
+
// a linking ':' glows only when both sides are angelic AND match (22:22, never 22:11).
|
|
133
|
+
// progressive tiers: pair glows, trio blazes, full house burns.
|
|
134
|
+
const tier = angels.filter(Boolean).length
|
|
135
|
+
const GLOW_PAIR = '0 0 24px rgba(255,230,0,0.45)'
|
|
136
|
+
const GLOW_TRIO =
|
|
137
|
+
'0 0 24px rgba(255,230,0,0.6), 0 0 64px rgba(255,230,0,0.35), 0 0 120px rgba(255,230,0,0.2)'
|
|
138
|
+
const GLOW_FULL =
|
|
139
|
+
'0 0 32px rgba(255,230,0,0.8), 0 0 90px rgba(255,230,0,0.45), 0 0 160px rgba(255,230,0,0.25)'
|
|
140
|
+
// NOTE: pair/trio/full intensities stay literal — GLOW.brandCult is the ambient
|
|
141
|
+
// brand glow; these are brighter presentation tiers for the countdown moment.
|
|
142
|
+
const tierGlow = tier >= 4 ? GLOW_FULL : tier === 3 ? GLOW_TRIO : GLOW_PAIR
|
|
82
143
|
const digitStyle = (angelic: boolean): CSSProperties =>
|
|
83
144
|
angelic
|
|
84
145
|
? {
|
|
85
146
|
fontFamily: 'var(--font-cult)',
|
|
86
147
|
color: 'var(--color-accent)',
|
|
87
|
-
textShadow:
|
|
148
|
+
textShadow: tierGlow,
|
|
88
149
|
}
|
|
89
150
|
: { fontFamily: 'var(--font-cult)', color: '#ffffff' }
|
|
151
|
+
const sepLinked = (i: number): boolean =>
|
|
152
|
+
angels[i] && angels[i + 1] && cells[i][0] === cells[i + 1][0]
|
|
153
|
+
const sepStyle = (linked: boolean): CSSProperties =>
|
|
154
|
+
linked
|
|
155
|
+
? {
|
|
156
|
+
fontFamily: 'var(--font-cult)',
|
|
157
|
+
color: 'var(--color-accent)',
|
|
158
|
+
textShadow: tierGlow,
|
|
159
|
+
}
|
|
160
|
+
: { fontFamily: 'var(--font-cult)' }
|
|
90
161
|
|
|
91
162
|
return (
|
|
92
163
|
<div className={cn('text-center', className)}>
|
|
@@ -95,7 +166,11 @@ export function CultCountdown({
|
|
|
95
166
|
aria-label={`Time until MeyCult Unveil, ${unveilDayLabel(resolved)}`}
|
|
96
167
|
aria-live="off"
|
|
97
168
|
aria-atomic="true"
|
|
98
|
-
className=
|
|
169
|
+
className={
|
|
170
|
+
tier >= 3
|
|
171
|
+
? 'countdown-bob flex items-start justify-center gap-2 sm:gap-4'
|
|
172
|
+
: 'flex items-start justify-center gap-2 sm:gap-4'
|
|
173
|
+
}
|
|
99
174
|
>
|
|
100
175
|
{cells.map(([v, l], i) => (
|
|
101
176
|
<div key={l} className="flex items-start gap-2 sm:gap-4">
|
|
@@ -106,7 +181,7 @@ export function CultCountdown({
|
|
|
106
181
|
<div aria-hidden="true" className="ui-label text-text-muted mt-1">{l}</div>
|
|
107
182
|
</div>
|
|
108
183
|
{i < cells.length - 1 && (
|
|
109
|
-
<div aria-hidden="true" className="text-5xl sm:text-7xl text-accent/40" style={
|
|
184
|
+
<div aria-hidden="true" className="text-5xl sm:text-7xl text-accent/40" style={sepStyle(sepLinked(i))}>:</div>
|
|
110
185
|
)}
|
|
111
186
|
</div>
|
|
112
187
|
))}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { User, Globe, Trophy, Bitcoin, Building2, Landmark } from 'lucide-react'
|
|
2
|
+
import type { Category, HeroType } from './types'
|
|
3
|
+
|
|
4
|
+
/** Quest taxonomy — single source (was duplicated in Temple/QuestDetail).
|
|
5
|
+
* Hexes stay literal: no theme tokens exist for categories (P0 lock). */
|
|
6
|
+
export const CATEGORY_COLORS: Record<Category, string> = {
|
|
7
|
+
POLITICS: '#3b82f6', TECH: '#8b5cf6', CRYPTO: '#f59e0b', SPORTS: '#22c55e', CULTURE: '#ec4899',
|
|
8
|
+
WORLD: '#dc2626', FINANCE: '#06b6d4', SCIENCE: '#6366f1', AI: '#d946ef', GAMING: '#84cc16', ENTERTAINMENT: '#f97316',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CATEGORY_LABELS: Record<Category, string> = {
|
|
12
|
+
POLITICS: 'Politics', TECH: 'Tech', CRYPTO: 'Crypto', SPORTS: 'Sports', CULTURE: 'Culture',
|
|
13
|
+
WORLD: 'World', FINANCE: 'Finance', SCIENCE: 'Science', AI: 'AI', GAMING: 'Gaming', ENTERTAINMENT: 'Entertainment',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const HERO_TYPE_LABELS: Record<HeroType, { label: string; icon: typeof User }> = {
|
|
17
|
+
person: { label: 'People', icon: User },
|
|
18
|
+
nation: { label: 'Nations', icon: Globe },
|
|
19
|
+
sports_team: { label: 'Sports', icon: Trophy },
|
|
20
|
+
crypto: { label: 'Crypto', icon: Bitcoin },
|
|
21
|
+
corporation: { label: 'Corps', icon: Building2 },
|
|
22
|
+
organization: { label: 'Orgs', icon: Landmark },
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { COLORS } from '../tokens'
|
|
2
|
+
import type { CultId, VirtueName } from './types'
|
|
3
|
+
|
|
4
|
+
/** The four cults — single source. Merged from 5 page-local copies
|
|
5
|
+
* (Temple/QuestDetail/HeroProfile/Profile/Onboarding). Colors double as the
|
|
6
|
+
* canonical virtue palette (each cult is element-bound to one virtue). */
|
|
7
|
+
export interface CultMeta {
|
|
8
|
+
id: CultId
|
|
9
|
+
name: string
|
|
10
|
+
element: string
|
|
11
|
+
description: string
|
|
12
|
+
color: string
|
|
13
|
+
virtue: VirtueName
|
|
14
|
+
virtueLabel: string
|
|
15
|
+
bonus: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CULT_META: Record<CultId, CultMeta> = {
|
|
19
|
+
driftless: {
|
|
20
|
+
id: 'driftless',
|
|
21
|
+
name: 'The Driftless',
|
|
22
|
+
element: 'Air',
|
|
23
|
+
description: 'Empty, yet inexhaustible. Masters of detachment who see through the fog.',
|
|
24
|
+
color: '#06b6d4',
|
|
25
|
+
virtue: 'clarity',
|
|
26
|
+
virtueLabel: 'Clarity',
|
|
27
|
+
bonus: '+10% when holding positions untouched for a cycle',
|
|
28
|
+
},
|
|
29
|
+
leviathan: {
|
|
30
|
+
id: 'leviathan',
|
|
31
|
+
name: 'Leviathan',
|
|
32
|
+
element: 'Water',
|
|
33
|
+
description: 'Grace flows downward. The lowest place is the highest.',
|
|
34
|
+
color: '#3b82f6',
|
|
35
|
+
virtue: 'humility',
|
|
36
|
+
virtueLabel: 'Humility',
|
|
37
|
+
bonus: 'Grace Pool: tithe winnings for mercy on ruin',
|
|
38
|
+
},
|
|
39
|
+
masonry: {
|
|
40
|
+
id: 'masonry',
|
|
41
|
+
name: 'The Masonry',
|
|
42
|
+
element: 'Earth',
|
|
43
|
+
description: 'Bear and forbear. The ground does not flee the storm.',
|
|
44
|
+
color: '#f59e0b',
|
|
45
|
+
virtue: 'endurance',
|
|
46
|
+
virtueLabel: 'Endurance',
|
|
47
|
+
bonus: 'Grounding Lock: outcome-independent returns',
|
|
48
|
+
},
|
|
49
|
+
recurrence: {
|
|
50
|
+
id: 'recurrence',
|
|
51
|
+
name: 'The Recurrence',
|
|
52
|
+
element: 'Fire',
|
|
53
|
+
description: 'Burn, and become. Loss is the only fuel that matters.',
|
|
54
|
+
color: '#ef4444',
|
|
55
|
+
virtue: 'overcoming',
|
|
56
|
+
virtueLabel: 'Overcoming',
|
|
57
|
+
bonus: 'Will-to-Power: losses multiply future payout',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const CULT_ORDER: CultId[] = ['driftless', 'leviathan', 'masonry', 'recurrence']
|
|
62
|
+
|
|
63
|
+
export const VIRTUE_ORDER: VirtueName[] = ['clarity', 'humility', 'endurance', 'overcoming']
|
|
64
|
+
|
|
65
|
+
/** Flat id→color map (Temple/HeroProfile shape). */
|
|
66
|
+
export const CULT_COLORS: Record<CultId, string> = {
|
|
67
|
+
driftless: CULT_META.driftless.color,
|
|
68
|
+
leviathan: CULT_META.leviathan.color,
|
|
69
|
+
masonry: CULT_META.masonry.color,
|
|
70
|
+
recurrence: CULT_META.recurrence.color,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fallback for unknown/uninitiated cult ids (was '#666'/'#6b7280' literals). */
|
|
74
|
+
export const FALLBACK_CULT: CultMeta = {
|
|
75
|
+
id: 'driftless',
|
|
76
|
+
name: 'Uninitiated',
|
|
77
|
+
element: '',
|
|
78
|
+
description: 'Join a cult to unlock your powers.',
|
|
79
|
+
color: COLORS.textMuted,
|
|
80
|
+
virtue: 'clarity',
|
|
81
|
+
virtueLabel: '',
|
|
82
|
+
bonus: '',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Safe lookup — never undefined. */
|
|
86
|
+
export function getCult(id: string): CultMeta {
|
|
87
|
+
return (CULT_META as Record<string, CultMeta>)[id] ?? FALLBACK_CULT
|
|
88
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Shared quest-display formatters — single source (Temple impls win).
|
|
2
|
+
*
|
|
3
|
+
* Rarity language note — three separate tracks, do NOT merge:
|
|
4
|
+
* - Rarity (common/uncommon/rare/mythic): ITEM rarity (relics, loot).
|
|
5
|
+
* - rarityTier() (Common…Mythic + Refined/Shifting): QUEST density scale.
|
|
6
|
+
* - VirtueTierLabel (shifting/refined/uncommon/rare/mythic): VIRTUE tier names.
|
|
7
|
+
* Same words, different axes. The overlap is game lore, not a bug. */
|
|
8
|
+
|
|
9
|
+
export interface DensityTier {
|
|
10
|
+
label: string
|
|
11
|
+
color: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Quest density → tier (thresholds 85/60/40/25/15). Colors stay literal (P0 lock). */
|
|
15
|
+
export function rarityTier(d: number): DensityTier {
|
|
16
|
+
if (d >= 85) return { label: 'Mythic', color: '#ef4444' }
|
|
17
|
+
if (d >= 60) return { label: 'Rare', color: '#f0c040' }
|
|
18
|
+
if (d >= 40) return { label: 'Uncommon', color: '#8b5cf6' }
|
|
19
|
+
if (d >= 25) return { label: 'Refined', color: '#3b82f6' }
|
|
20
|
+
if (d >= 15) return { label: 'Shifting', color: '#22c55e' }
|
|
21
|
+
return { label: 'Common', color: '#9ca3af' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatVolume(v: number): string {
|
|
25
|
+
if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`
|
|
26
|
+
if (v >= 1000) return `${(v / 1000).toFixed(0)}K`
|
|
27
|
+
return `${v}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Deadline {
|
|
31
|
+
text: string
|
|
32
|
+
urgent: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Rich deadline (Temple impl): needs `now` for live ticking. */
|
|
36
|
+
export function formatDeadline(closesIn: string, now: number): Deadline {
|
|
37
|
+
if (!closesIn) return { text: '', urgent: false }
|
|
38
|
+
const d = new Date(closesIn)
|
|
39
|
+
if (isNaN(d.getTime())) return { text: '', urgent: false }
|
|
40
|
+
const diff = d.getTime() - now
|
|
41
|
+
if (diff < 0) return { text: 'Closed', urgent: true }
|
|
42
|
+
const days = Math.floor(diff / 86400000)
|
|
43
|
+
const hours = Math.floor((diff % 86400000) / 3600000)
|
|
44
|
+
const mins = Math.floor((diff % 3600000) / 60000)
|
|
45
|
+
const secs = Math.floor((diff % 60000) / 1000)
|
|
46
|
+
if (days >= 2) return { text: `Closes ${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`, urgent: false }
|
|
47
|
+
if (days >= 1) return { text: `Ends in ${days}d ${hours}h`, urgent: true }
|
|
48
|
+
if (hours >= 1) return { text: `Ends in ${hours}h ${mins}m`, urgent: true }
|
|
49
|
+
return { text: `Ends in ${mins}m ${secs}s`, urgent: true }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Short deadline string (QuestDetail impl): no `now` needed. */
|
|
53
|
+
export function formatDeadlineShort(closesIn: string): string {
|
|
54
|
+
const d = new Date(closesIn)
|
|
55
|
+
if (isNaN(d.getTime())) return closesIn
|
|
56
|
+
const diff = d.getTime() - Date.now()
|
|
57
|
+
if (diff < 0) return 'Closed'
|
|
58
|
+
const days = Math.floor(diff / 86400000)
|
|
59
|
+
if (days > 0) return `${days}d remaining`
|
|
60
|
+
const hours = Math.floor(diff / 3600000)
|
|
61
|
+
return `${hours}h remaining`
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Shared Cult game types. App re-exports these from @/types (compat) —
|
|
2
|
+
* the game-specific remainder (Quest, Hero, Player, Wager, …) stays app-owned
|
|
3
|
+
* until a second brand needs it. */
|
|
4
|
+
|
|
5
|
+
export type VirtueName = 'clarity' | 'humility' | 'endurance' | 'overcoming'
|
|
6
|
+
|
|
7
|
+
export type Virtues = Record<VirtueName, number>
|
|
8
|
+
|
|
9
|
+
export type VirtueXP = Record<VirtueName, number>
|
|
10
|
+
|
|
11
|
+
export type VirtueTierLevel = 0 | 1 | 2 | 3
|
|
12
|
+
|
|
13
|
+
export type VirtueTierLabel = 'shifting' | 'refined' | 'uncommon' | 'rare' | 'mythic'
|
|
14
|
+
|
|
15
|
+
export type CultId = 'driftless' | 'leviathan' | 'masonry' | 'recurrence'
|
|
16
|
+
|
|
17
|
+
export type Category = 'POLITICS' | 'TECH' | 'CRYPTO' | 'SPORTS' | 'CULTURE' | 'WORLD' | 'FINANCE' | 'SCIENCE' | 'AI' | 'GAMING' | 'ENTERTAINMENT'
|
|
18
|
+
|
|
19
|
+
export type HeroType = 'person' | 'nation' | 'sports_team' | 'crypto' | 'corporation' | 'organization'
|
|
20
|
+
|
|
21
|
+
export type PredictionResult = 'PENDING' | 'WON' | 'LOST'
|
|
22
|
+
|
|
23
|
+
export type Rarity = 'common' | 'uncommon' | 'rare' | 'mythic'
|
|
24
|
+
|
|
25
|
+
export type BadgeTier = 'bronze' | 'silver' | 'gold' | 'platinum'
|
|
26
|
+
|
|
27
|
+
export type BadgeCategory = 'VIRTUE' | 'LOYALTY' | 'COLLECTION' | 'MARKET'
|