diff --git a/.gitignore b/.gitignore index b027d2e..373af44 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ dist # Data data/ +!src/landing/src/data/ *.env diff --git a/Dockerfile b/Dockerfile index fd3ee70..f4ba7ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,9 @@ COPY . . RUN npm --prefix src/frontend ci RUN npm --prefix src/frontend run build:frontend +# Build the static landing site into src/landing/dist +RUN npm run build:landing + USER node EXPOSE 80 diff --git a/package.json b/package.json index 35709f6..fc8e3bb 100644 --- a/package.json +++ b/package.json @@ -14,24 +14,25 @@ "npm": ">=8.0.0" }, "scripts": { - "dev": "tsx watch src/main.ts", + "dev": "npm run build:landing && tsx watch src/main.ts", "tunnel": "ngrok http --domain dominant-annually-lobster.ngrok-free.app 3000", "xtunnel": "xtunnel http 3000", "build:bot": "tsc --noEmit false", "build:frontend": "npm --prefix src/frontend run build", - "build:all": "npm run build:bot && npm run build:frontend", + "build:landing": "tsx scripts/build-landing.ts", + "build:all": "npm run build:bot && npm run build:landing && npm run build:frontend", "update:bot": "npx npm-check-updates -u && npm install", "update:frontend": "cd src/frontend && npx npm-check-updates -u && npm install", "update:all": "npm run update:bot && npm run update:frontend", "prepare": "npx husky", - "lint": "eslint . --ignore-pattern .github/ --ignore-pattern docs/", + "lint": "eslint . --ignore-pattern .github/ --ignore-pattern docs/ --ignore-pattern .superpowers/", "format": "prettier --write \"**/*.{ts,js,vue,json,css,scss,md}\"", "format:check": "prettier --check \"**/*.{ts,js,vue,json,css,scss,md}\"", "start": "tsc && tsx ./src/main.ts", "start:force": "tsx ./src/main.ts", "typecheck": "tsc", "test": "npm run test:backend", - "test:backend": "NODE_ENV=test node --import tsx --test $(find src -name '*.test.ts' -not -path 'src/frontend/*')", + "test:backend": "NODE_ENV=test node --import tsx --test scripts/landing/*.test.ts $(find src -name '*.test.ts' -not -path 'src/frontend/*')", "test:coverage": "NODE_ENV=test node --import tsx --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/frontend/**' $(find src -name '*.test.ts' -not -path 'src/frontend/*')" }, "dependencies": { diff --git a/scripts/build-landing.ts b/scripts/build-landing.ts new file mode 100644 index 0000000..d3475ad --- /dev/null +++ b/scripts/build-landing.ts @@ -0,0 +1,203 @@ +import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { parseFrontMatter } from './landing/frontmatter' +import { renderMarkdown } from './landing/markdown' +import { renderTemplate } from './landing/template' + +const ROOT = path.dirname(fileURLToPath(import.meta.url)) +const SRC = path.join(ROOT, '../src/landing/src') +const PUBLIC = path.join(ROOT, '../src/landing/public') +const OUT = path.join(ROOT, '../src/landing/dist') + +interface Site { + brand: string + telegramBot: string + contact: string + baseUrl: string + nav: { slug: string, label: string }[] + footer: { slug: string, label: string }[] + metricsFallback: Record +} + +async function readText(p: string): Promise { + return readFile(p, 'utf-8') +} + +function href(slug: string): string { + return slug === '' ? '/' : `/${slug}/` +} + +function navLinksHtml(site: Site, active: string): string { + return site.nav + .map((n) => { + const cls = n.slug === active ? ' class="active"' : '' + return `${n.label}` + }) + .join('\n ') +} + +function footerLinksHtml(site: Site): string { + return site.footer + .map(n => `${n.label}`) + .join('\n ') +} + +async function main() { + const site: Site = JSON.parse(await readText(path.join(SRC, 'data/site.json'))) + const layout = await readText(path.join(SRC, 'layout.html')) + const partials = { + nav: await readText(path.join(SRC, 'partials/nav.html')), + footer: await readText(path.join(SRC, 'partials/footer.html')), + } + + await rm(OUT, { recursive: true, force: true }) + await mkdir(OUT, { recursive: true }) + + const urls: string[] = [] + + const emit = async ( + outRel: string, + content: string, + meta: { title: string, description: string, active?: string, extraCss?: string }, + ) => { + const html = renderTemplate( + layout, + { + title: meta.title, + description: meta.description, + extraCss: meta.extraCss ?? '', + content, + navLinks: navLinksHtml(site, meta.active ?? ''), + footerLinks: footerLinksHtml(site), + }, + partials, + ) + const outPath = path.join(OUT, outRel) + await mkdir(path.dirname(outPath), { recursive: true }) + await writeFile(outPath, html) + const url = outRel.replace(/index\.html$/, '').replace(/\\/g, '/') + urls.push(`/${url}`) + } + + // 1) HTML page fragments in pages/ + const pageDir = path.join(SRC, 'pages') + for (const file of await readdir(pageDir)) { + if (!file.endsWith('.html')) { + continue + } + const name = file.replace(/\.html$/, '') + const raw = await readText(path.join(pageDir, file)) + const { data, body } = parseFrontMatter(raw) + const outRel = name === 'home' ? 'index.html' : path.join(name, 'index.html') + await emit(outRel, body, { + title: data.title ?? site.brand, + description: data.description ?? '', + active: data.active, + extraCss: data.extraCss === 'app' + ? '' + : '', + }) + } + + // 2) Markdown content: history + legal (single files → /index.html) + const renderMdFile = async (file: string, outRel: string, active: string) => { + const raw = await readText(file) + const { data, body } = parseFrontMatter(raw) + const article = `
${renderMarkdown(body)}
` + await emit(outRel, article, { + title: data.title ?? site.brand, + description: data.description ?? '', + active, + extraCss: data.extraCss === 'app' + ? '' + : '', + }) + } + + const historyDir = path.join(SRC, 'content/history') + for (const file of await readdir(historyDir)) { + if (!file.endsWith('.md')) { + continue + } + const name = file.replace(/\.md$/, '') + const outRel = name === 'index' ? path.join('history', 'index.html') : path.join('history', name, 'index.html') + await renderMdFile(path.join(historyDir, file), outRel, 'history') + } + + const legalDir = path.join(SRC, 'content/legal') + for (const file of await readdir(legalDir)) { + if (!file.endsWith('.md')) { + continue + } + const name = file.replace(/\.md$/, '') + await renderMdFile(path.join(legalDir, file), path.join(name, 'index.html'), '') + } + + // 3) Blog: posts + generated index + const blogDir = path.join(SRC, 'content/blog') + const posts: { slug: string, title: string, date: string, description: string }[] = [] + for (const file of await readdir(blogDir)) { + if (!file.endsWith('.md')) { + continue + } + const raw = await readText(path.join(blogDir, file)) + const { data, body } = parseFrontMatter(raw) + const slug = data.slug ?? file.replace(/\.md$/, '') + const outPath = path.join(OUT, 'blog', slug, 'index.html') + if (!outPath.startsWith(OUT + path.sep)) { + throw new Error(`Unsafe slug in ${file}: ${slug}`) + } + posts.push({ slug, title: data.title ?? slug, date: data.date ?? '', description: data.description ?? '' }) + const article = `
` + + `

${data.date ?? ''}

` + + `${renderMarkdown(body)}` + + `

← Back to devlog

` + await emit(path.join('blog', slug, 'index.html'), article, { + title: `${data.title ?? slug} — Cube Worlds`, + description: data.description ?? '', + active: 'blog', + }) + } + posts.sort((a, b) => (a.date < b.date ? 1 : -1)) + const blogList = posts + .map(p => `` + + `

${p.title}

${p.date}

` + + `

${p.description}

`) + .join('\n') + const blogIndex = `
` + + `
Devlog

Build updates

` + + `
${blogList}
` + await emit(path.join('blog', 'index.html'), blogIndex, { + title: 'Devlog — Cube Worlds', + description: 'Development updates from Cube Worlds.', + active: 'blog', + }) + + // 4) Static assets + await cp(path.join(SRC, 'styles'), path.join(OUT, 'styles'), { recursive: true }) + await cp(PUBLIC, OUT, { recursive: true }) + + // 5) 404 page (styled with chrome) + const notFound = `
` + + `

404

That page drifted off into the cosmos.

` + + `
` + await emit('404.html', notFound, { title: 'Not found — Cube Worlds', description: 'Page not found.' }) + + // 6) robots + sitemap + await writeFile(path.join(OUT, 'robots.txt'), `User-agent: *\nAllow: /\nSitemap: ${site.baseUrl}/sitemap.xml\n`) + const sitemapUrls = [...new Set(urls)] + .filter(u => !u.endsWith('404.html')) + .map(u => ` ${site.baseUrl}${u}`) + .join('\n') + const sitemap = `\n\n${sitemapUrls}\n\n` + await writeFile(path.join(OUT, 'sitemap.xml'), sitemap) + + process.stdout.write(`Landing built: ${new Set(urls).size} pages → ${path.relative(process.cwd(), OUT)}\n`) +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`) + process.exitCode = 1 +}) diff --git a/scripts/landing/frontmatter.test.ts b/scripts/landing/frontmatter.test.ts new file mode 100644 index 0000000..1c9f76f --- /dev/null +++ b/scripts/landing/frontmatter.test.ts @@ -0,0 +1,23 @@ +/* eslint-disable test/no-import-node-test */ +import assert from 'node:assert/strict' +import test from 'node:test' +import { parseFrontMatter } from './frontmatter' + +test('parses a front-matter block and returns the body', () => { + const raw = '---\ntitle: Hello\ndescription: A page\n---\n# Body\ntext' + const { data, body } = parseFrontMatter(raw) + assert.equal(data.title, 'Hello') + assert.equal(data.description, 'A page') + assert.equal(body, '# Body\ntext') +}) + +test('returns empty data when there is no front matter', () => { + const { data, body } = parseFrontMatter('# Just body') + assert.deepEqual(data, {}) + assert.equal(body, '# Just body') +}) + +test('values containing colons are preserved after the first colon', () => { + const { data } = parseFrontMatter('---\ntitle: A: B: C\n---\nx') + assert.equal(data.title, 'A: B: C') +}) diff --git a/scripts/landing/frontmatter.ts b/scripts/landing/frontmatter.ts new file mode 100644 index 0000000..b8bb8c0 --- /dev/null +++ b/scripts/landing/frontmatter.ts @@ -0,0 +1,24 @@ +export interface FrontMatter { + data: Record + body: string +} + +export function parseFrontMatter(raw: string): FrontMatter { + const match = /^---\n([\s\S]*?)\n---\n?/.exec(raw) + if (!match) { + return { data: {}, body: raw } + } + const data: Record = {} + for (const line of match[1].split('\n')) { + const idx = line.indexOf(':') + if (idx === -1) { + continue + } + const key = line.slice(0, idx).trim() + const value = line.slice(idx + 1).trim() + if (key) { + data[key] = value + } + } + return { data, body: raw.slice(match[0].length) } +} diff --git a/scripts/landing/markdown.test.ts b/scripts/landing/markdown.test.ts new file mode 100644 index 0000000..d928433 --- /dev/null +++ b/scripts/landing/markdown.test.ts @@ -0,0 +1,33 @@ +/* eslint-disable test/no-import-node-test */ +import assert from 'node:assert/strict' +import test from 'node:test' +import { renderMarkdown } from './markdown' + +test('renders headings', () => { + assert.equal(renderMarkdown('## Title'), '

Title

') +}) + +test('renders a paragraph with inline formatting', () => { + assert.equal( + renderMarkdown('A **bold** and *italic* and `code` word'), + '

A bold and italic and code word

', + ) +}) + +test('renders links and images', () => { + assert.equal(renderMarkdown('[T](https://x.io)'), '

T

') + assert.equal(renderMarkdown('![alt](/a.png)'), '

alt

') +}) + +test('renders an unordered list', () => { + assert.equal(renderMarkdown('- one\n- two'), '
  • one
  • two
') +}) + +test('renders blockquote and hr', () => { + assert.equal(renderMarkdown('> quote'), '
quote
') + assert.equal(renderMarkdown('---'), '
') +}) + +test('escapes raw HTML in text', () => { + assert.equal(renderMarkdown('a < b & c'), '

a < b & c

') +}) diff --git a/scripts/landing/markdown.ts b/scripts/landing/markdown.ts new file mode 100644 index 0000000..2043b8a --- /dev/null +++ b/scripts/landing/markdown.ts @@ -0,0 +1,78 @@ +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') +} + +// Inline formatting runs on already-escaped text. Images before links +// (both use the [] () shape). Placeholders are not needed because the +// replacements do not re-introduce markdown syntax. +function inline(text: string): string { + return text + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') +} + +export function renderMarkdown(md: string): string { + const lines = md.replace(/\r\n/g, '\n').split('\n') + const out: string[] = [] + let para: string[] = [] + let list: string[] = [] + + const flushPara = () => { + if (para.length) { + out.push(`

${inline(escapeHtml(para.join(' ')))}

`) + para = [] + } + } + const flushList = () => { + if (list.length) { + out.push(`
    ${list.map(i => `
  • ${inline(escapeHtml(i))}
  • `).join('')}
`) + list = [] + } + } + + for (const line of lines) { + const trimmed = line.trim() + if (trimmed === '') { + flushPara() + flushList() + continue + } + if (/^-{2,}$/.test(trimmed)) { + flushPara() + flushList() + out.push('
') + continue + } + const headingMatch = /^(#{1,6})\s/.exec(trimmed) + if (headingMatch) { + flushPara() + flushList() + const level = headingMatch[1].length + const content = trimmed.slice(level + 1).trim() + out.push(`${inline(escapeHtml(content))}`) + continue + } + if (trimmed.startsWith('> ')) { + flushPara() + flushList() + out.push(`
${inline(escapeHtml(trimmed.slice(2)))}
`) + continue + } + if (trimmed.startsWith('- ')) { + flushPara() + list.push(trimmed.slice(2)) + continue + } + flushList() + para.push(trimmed) + } + flushPara() + flushList() + return out.join('') +} diff --git a/scripts/landing/template.test.ts b/scripts/landing/template.test.ts new file mode 100644 index 0000000..6e3949d --- /dev/null +++ b/scripts/landing/template.test.ts @@ -0,0 +1,17 @@ +/* eslint-disable test/no-import-node-test */ +import assert from 'node:assert/strict' +import test from 'node:test' +import { renderTemplate } from './template' + +test('substitutes variables', () => { + assert.equal(renderTemplate('

{{title}}

', { title: 'Hi' }), '

Hi

') +}) + +test('missing variables render empty', () => { + assert.equal(renderTemplate('a{{missing}}b', {}), 'ab') +}) + +test('includes partials, which may themselves contain variables', () => { + const out = renderTemplate('{{> nav}}|{{body}}', { body: 'X', active: 'home' }, { nav: '[{{active}}]' }) + assert.equal(out, '[home]|X') +}) diff --git a/scripts/landing/template.ts b/scripts/landing/template.ts new file mode 100644 index 0000000..c1c4768 --- /dev/null +++ b/scripts/landing/template.ts @@ -0,0 +1,15 @@ +export function renderTemplate( + tpl: string, + vars: Record, + partials: Record = {}, +): string { + // Expand partials first (single nesting level is enough for this site). + const withPartials = tpl.replace( + /\{\{>\s*([\w-]+)\s*\}\}/g, + (_m, name: string) => partials[name] ?? '', + ) + return withPartials.replace( + /\{\{\s*([\w-]+)\s*\}\}/g, + (_m, key: string) => vars[key] ?? '', + ) +} diff --git a/src/backend/public-metrics-handler.test.ts b/src/backend/public-metrics-handler.test.ts new file mode 100644 index 0000000..54e639e --- /dev/null +++ b/src/backend/public-metrics-handler.test.ts @@ -0,0 +1,52 @@ +/* eslint-disable test/no-import-node-test */ +import type { PublicMetricsHandlerDependencies } from '#root/backend/public-metrics-handler' +import assert from 'node:assert/strict' +import test from 'node:test' +import fastify from 'fastify' +import { buildPublicMetricsHandler } from '#root/backend/public-metrics-handler' + +function ctx(overrides: Partial = {}) { + let calls = 0 + let clock = 1000 + const deps: PublicMetricsHandlerDependencies = { + fetchMetrics: () => { + calls += 1 + return Promise.resolve({ players: 10, minted: 3, paidOutMicroUsdt: '5000000', activeWeek: 4 }) + }, + now: () => clock, + cacheTtlMs: 60_000, + ...overrides, + } + const app = fastify() + return { + app, + deps, + get calls() { + return calls + }, + setClock: (v: number) => { + clock = v + }, + } +} + +test('GET /metrics returns the metrics shape', async (t) => { + const c = ctx() + await c.app.register(buildPublicMetricsHandler(c.deps), { prefix: '/api/public' }) + t.after(() => c.app.close()) + const res = await c.app.inject({ method: 'GET', url: '/api/public/metrics' }) + assert.equal(res.statusCode, 200) + assert.deepEqual(res.json(), { players: 10, minted: 3, paidOutMicroUsdt: '5000000', activeWeek: 4 }) +}) + +test('caches within the TTL and refetches after it', async (t) => { + const c = ctx() + await c.app.register(buildPublicMetricsHandler(c.deps), { prefix: '/api/public' }) + t.after(() => c.app.close()) + await c.app.inject({ method: 'GET', url: '/api/public/metrics' }) + await c.app.inject({ method: 'GET', url: '/api/public/metrics' }) + assert.equal(c.calls, 1) + c.setClock(1000 + 60_001) + await c.app.inject({ method: 'GET', url: '/api/public/metrics' }) + assert.equal(c.calls, 2) +}) diff --git a/src/backend/public-metrics-handler.ts b/src/backend/public-metrics-handler.ts new file mode 100644 index 0000000..e803c13 --- /dev/null +++ b/src/backend/public-metrics-handler.ts @@ -0,0 +1,46 @@ +import type { FastifyInstance } from 'fastify' + +export interface PublicMetrics { + players: number + minted: number + paidOutMicroUsdt: string + activeWeek: number +} + +export interface PublicMetricsHandlerDependencies { + fetchMetrics: () => Promise + now: () => number + cacheTtlMs: number +} + +function createDefaultDependencies(): PublicMetricsHandlerDependencies { + // Real data-fetchers are injected by the composer (public-metrics.ts) to + // keep this module free of any #root/config transitive import. + return { + fetchMetrics: () => + Promise.resolve({ players: 0, minted: 0, paidOutMicroUsdt: '0', activeWeek: 0 }), + now: () => Date.now(), + cacheTtlMs: 60_000, + } +} + +export function buildPublicMetricsHandler( + dependencies: PublicMetricsHandlerDependencies = createDefaultDependencies(), +) { + let cached: { at: number, value: PublicMetrics } | null = null + + return async function publicMetricsHandler(fastify: FastifyInstance) { + fastify.get('/metrics', async () => { + if (cached && dependencies.now() - cached.at < dependencies.cacheTtlMs) { + return cached.value + } + const value = await dependencies.fetchMetrics() + cached = { at: dependencies.now(), value } + return value + }) + } +} + +const publicMetricsHandler = buildPublicMetricsHandler() + +export default publicMetricsHandler diff --git a/src/backend/public-metrics.ts b/src/backend/public-metrics.ts new file mode 100644 index 0000000..9122549 --- /dev/null +++ b/src/backend/public-metrics.ts @@ -0,0 +1,30 @@ +import type { PublicMetrics } from '#root/backend/public-metrics-handler' +import { buildPublicMetricsHandler } from '#root/backend/public-metrics-handler' +import { totalPaidOut } from '#root/common/models/RewardsPoolLedger' +import { countActiveSince, countAllWallets, countMinted } from '#root/common/models/User' + +const WEEK_MS = 7 * 24 * 60 * 60 * 1000 + +async function fetchMetrics(): Promise { + const since = new Date(Date.now() - WEEK_MS) + const [players, minted, paidOut, activeWeek] = await Promise.all([ + countAllWallets(), + countMinted(), + totalPaidOut(), + countActiveSince(since), + ]) + return { + players, + minted, + paidOutMicroUsdt: paidOut.toString(), + activeWeek, + } +} + +const publicMetricsHandler = buildPublicMetricsHandler({ + fetchMetrics, + now: () => Date.now(), + cacheTtlMs: 60_000, +}) + +export default publicMetricsHandler diff --git a/src/common/models/RewardsPoolLedger.ts b/src/common/models/RewardsPoolLedger.ts index 02548df..f581bbb 100644 --- a/src/common/models/RewardsPoolLedger.ts +++ b/src/common/models/RewardsPoolLedger.ts @@ -84,3 +84,9 @@ export async function poolBalance(): Promise { const rows = await RewardsPoolLedgerModel.find({ currency: WALLET_CURRENCY }) return rows.reduce((total, row) => total + row.amount, 0n) } + +// Total USDT paid out to players, as positive micro-USDT. +export async function totalPaidOut(): Promise { + const rows = await RewardsPoolLedgerModel.find({ type: RewardsEntryType.Payout }) + return rows.reduce((total, row) => total + (row.amount < 0n ? -row.amount : 0n), 0n) +} diff --git a/src/common/models/User.ts b/src/common/models/User.ts index c279879..946f3c3 100644 --- a/src/common/models/User.ts +++ b/src/common/models/User.ts @@ -289,6 +289,10 @@ export function countAllWallets(): Promise { return UserModel.countDocuments({ wallet: { $exists: true } }) } +export function countActiveSince(since: Date): Promise { + return UserModel.countDocuments({ updatedAt: { $gte: since } }) +} + export function countAllLine(): Promise { return UserModel.countDocuments({ state: UserState.Submited, diff --git a/src/landing/index.html b/src/landing/index.html deleted file mode 100644 index c16e46d..0000000 --- a/src/landing/index.html +++ /dev/null @@ -1,746 +0,0 @@ - - - - - -Cube Worlds — Ancient Worlds ARPG on TON - - - - - - - - - - - - -
-
- - - - - -
-
- ⛓️ Built on TON · 🎮 Playable inside Telegram -

Build an empire in the Ancient Worlds

-

- Cube Worlds is an NFT-gated idle-ARPG that lives entirely inside Telegram. Raise a castle, - recruit heroes, raid rivals, hunt the weekly boss, and compete for real USDT prize pools — - all powered by the $CUBE economy on TON. -

- -

Own a Cube Worlds NFT to enter · Free to start · No download

- -
-
🏰 Castles & production
-
🛡️ Heroes & PvE dungeons
-
🗡️ Arena & raids
-
🐉 Weekly boss
-
🏅 USDT tournaments
-
-
-
- - -
-
-
-
The Game
-

A full ARPG loop, in chat

-

Every system is on-chain-ready and DB-canonical. Progress compounds while you're away and pays off when you return.

-
-
-
🏰

Your Castle

Four upgrade tracks — Mine, Walls, Forge, Tavern. Resources accrue on an 8-hour production tick. Founders earn +20%.

-
🛡️

Heroes & Dungeons

Recruit knights, mages, archers and rogues at the Tavern. Run the deterministic daily dungeon and 8-hour quests for XP and loot.

-
⚔️

Equipment

Four slots × four rarities. Gear folds directly into combat. Rare drops from quests and the weekly boss.

-
🗡️

Arena & Raids

Async-snapshot PvP on a single ELO ladder. Raid rival castles to plunder resources — or shield up after you're hit.

-
🐉

Weekly Boss

The whole server chips away at a shared boss. Top the damage board for legendary, epic and rare equipment tiers.

-
⚔️

Expeditions

Spend energy to dispatch expeditions across five cube-worlds. A congestion model dilutes crowded worlds — pick your risk.

-
🏅

Tournaments

Monday-aligned weekly tournaments with real USDT prize pools, funded by the rewards pool and paid out via xRocket.

-
🎟️

NFT-gated entry

Generate a pixel-art NFT, climb the mint queue with votes, and own your seat in the world before you play.

-
-
-
- - -
-
-
-
Economy & Sustainability
-

Designed for the long game

-

A deliberately conservative token model: sinks before faucets, no premature listing, and a rewards pool funded by real revenue.

-
-
-
-

The $CUBE model

-
    -
  • $CUBE is DB-only. Votes come from daily claims, referrals and TON donations — no on-chain jetton, no premature TGE.
  • -
  • 🎟️NFT-gated. Owning a Cube Worlds NFT gates game entry. An escalating mint floor keeps supply honest as the collection grows.
  • -
  • 🔥Sinks before faucets. Energy refills, weight-boosts, hero recruitment, castle upgrades and tournament entry all burn $CUBE before any faucet turns on.
  • -
  • 💰20% rewards pool. A fifth of net revenue accrues to an append-only, idempotent rewards ledger in micro-USDT that funds player payouts.
  • -
  • 🏆Skill pays. Weekly tournaments rank by in-window expedition CUBE and pay winners real USDT via free xRocket transfers.
  • -
-
-
-
-
TON
Settlement chain
-
20%
Revenue → rewards pool
-
4
NFT collections
-
100%
In-Telegram, no download
-
-
-

Monetization rails

-
-
xRocket USDT deposits, withdrawals & payouts
-
Telegram Stars Season Pass
-
Adsgram rewarded ads → energy
-
TON donations → mint votes
-
-

All USDT is held as bigint micro-USDT with an overdraft-proof ledger and an hourly custody reconciliation guard.

-
-
-
-
-
- - -
-
-
-
Product Tour
-

Every screen, as players see it

-

These are faithful mockups of the live Telegram Mini App — the same cosmic UI, balance bar and emoji navigation. The game itself runs only inside Telegram.

-
- -
- - - - - -
- - -
-
- - -
-
-
-
Getting Started
-

Three steps to your empire

-

No app store, no seed phrase gymnastics. Open the bot and you're in.

-
-
-
1

Open in Telegram

Launch @cube_worlds_bot and tap Play. The Mini App opens right in your chat — nothing to install.

-
2

Connect & mint

Bind your TON wallet, generate your pixel-art NFT, and earn votes from donations to clear the mint floor.

-
3

Play & earn

Build your castle, recruit heroes, raid rivals, and climb the weekly tournament for real USDT payouts.

-
-
-
- - -
-
-
-
Questions
-

Good to know

-
-
-
Where do I actually play?

Entirely inside Telegram. This page is an information hub for players and investors — the game runs as a Mini App in @cube_worlds_bot. There's nothing playable on this website by design.

-
Is $CUBE a tradable token?

No. $CUBE is a DB-only in-game currency measured in "votes." It drives NFT mint eligibility and in-game economy sinks. There is no on-chain jetton and no premature token listing.

-
Why do I need an NFT to play?

Owning a Cube Worlds NFT gates entry to the game. You generate a pixel-art NFT in-app and clear an escalating mint floor with votes earned from daily claims, referrals and TON donations.

-
How are USDT prizes paid?

Weekly tournaments pay real USDT via the xRocket rail. The prize pool is funded by a rewards pool that accrues 20% of net revenue, tracked in an idempotent, overdraft-proof micro-USDT ledger.

-
What can I spend money on?

Optional: energy packs and a Telegram Stars Season Pass. You can also watch rewarded ads for free energy. Everything is designed so sinks exist before any token faucet is enabled.

-
What chain is this on?

The Open Network (TON). NFTs, wallet binding (ton_proof), and donations are on-chain; game state is DB-canonical and on-chain-ready for the collections that are deployed.

-
-
-
- - -
-
-
-

Ready to raise your castle?

-

Join the Ancient Worlds. Mint your NFT, gather your heroes, and compete for the weekly USDT pool.

- ✈️ Open Cube Worlds in Telegram -
-
-
- - -
-
-
Cube Worlds
-
- Telegram - Game - Economy - FAQ -
-
-

- Cube Worlds is a game on The Open Network (TON). $CUBE is an in-game, database-only currency and is not a - financial instrument, security, or tradable token. NFTs and in-game items are for entertainment. Nothing on this - page is investment advice. Play responsibly. -

-
- - - - diff --git a/src/landing/logo.png b/src/landing/public/logo.png similarity index 100% rename from src/landing/logo.png rename to src/landing/public/logo.png diff --git a/src/landing/tonconnect-manifest.json b/src/landing/public/tonconnect-manifest.json similarity index 100% rename from src/landing/tonconnect-manifest.json rename to src/landing/public/tonconnect-manifest.json diff --git a/src/landing/src/content/blog/2026-07-06-hello.md b/src/landing/src/content/blog/2026-07-06-hello.md new file mode 100644 index 0000000..1ef4298 --- /dev/null +++ b/src/landing/src/content/blog/2026-07-06-hello.md @@ -0,0 +1,54 @@ +--- +title: Cube Worlds gets a real website +date: 2026-07-06 +slug: hello +description: We shipped a full multi-page landing site for Cube Worlds — here is what is on it and where the game is heading for v3. +--- + +# Cube Worlds gets a real website + +Until recently, Cube Worlds lived almost entirely inside Telegram. The game itself still does — that is by design — but for the first time the project now has a proper public-facing home at [cubeworlds.club](https://cubeworlds.club). + +## What we built + +The site is a static multi-page landing generated from HTML fragments and Markdown content. No JavaScript framework, no bundler for the pages themselves — just a small Node.js generator (about 200 lines) that reads page fragments and Markdown files, wraps them in a shared layout, and writes clean-URL `index.html` files to `dist/`. + +The pages that shipped: + +- **Home** — hero section, live stats pulled from the game API, and a grid of links to everything else. +- **Gameplay** — a full walkthrough of every game loop: castles, heroes, dungeons, quests, arena PvP, weekly boss, expeditions. +- **Economy** — the $CUBE model explained honestly: DB-only currency, escalating mint floor, 20% rewards pool, USDT prizes, Season Pass, and the sinks-before-faucet invariant. +- **Screens** — a filterable phone-frame gallery of every Mini App screen as players see it, including all nine mint flow states. +- **History** — the full development timeline from the original $CUBE concept through the xRocket money rail to Phase C arena and raids. +- **Investors & Partners** — traction numbers, the economy model in brief, the seed round ask, four NFT collab tracks, and a B2B dev-services section. +- **FAQ** — the most common questions answered plainly. +- **Press / Media Kit** — brand palette, boilerplate paragraph, logo download, screenshot gallery link, and contact. +- **Blog** (this page) — a devlog for build updates and design notes. +- **Privacy & Terms** — real policies, not stubs. + +## The generator + +Building a custom static generator for a site this size took less effort than configuring any off-the-shelf SSG to do exactly what we needed. The generator has three moving parts: + +1. **Front-matter parser** — a 20-line YAML-lite parser that strips and parses the `---` block at the top of each file. +2. **Markdown renderer** — a minimal converter handling headings, bold, italic, inline code, code blocks, links, and lists. No dependency on `marked`, `remark`, or any heavy AST pipeline. +3. **Template renderer** — a `{{slot}}` substitution pass that drops page content into the shared layout with the correct nav active state. + +The build runs in under a second. The output is a folder of plain HTML files with no client-side routing required. + +## Where the game is heading: the road to v3 + +Phase C (Arena + Raids) shipped earlier this year — async-snapshot PvP, a unified Match engine, the ELO ladder, and resource plunder. That closed out the core game loop: build → progress → compete → earn. + +The next milestones on the road to v3: + +1. **Live metrics API.** The stat tiles on the home and investors pages should show real numbers, not hardcoded ones. A small read-only API endpoint — gated behind a cache layer and served through the same Fastify backend — will replace the fallback values. +2. **Faucet enable.** The expedition CUBE faucet has been wired but intentionally off until the three sinks (energy refill, weight-boost, tournament entry) were confirmed live. They are. The flip will go out in the v3 release. +3. **Phase D — clans, trading post, Stars-gated gacha.** The next gameplay expansion: clan system, equipment trading post (5% CUBE burn on every trade), and Stars-based hero gacha for the players who want to skip the recruitment grind. +4. **Contract deploy + audit.** Castle and Hero NFT collections are wired in code; the contracts need to be deployed to mainnet and the addresses set in config before the mint runners activate. + +This site is the last piece before v3 ships. Everything else is in code — it just needs the switches flipped in the right order. + +--- + +Development questions or feedback? The fastest path is **@babin** on Telegram. diff --git a/src/landing/src/content/history/index.md b/src/landing/src/content/history/index.md new file mode 100644 index 0000000..a421afc --- /dev/null +++ b/src/landing/src/content/history/index.md @@ -0,0 +1,16 @@ +--- +title: History — Cube Worlds +description: How Cube Worlds evolved from a commands-only bot to an Ancient Worlds ARPG, and where it's headed. +--- +# The story so far + +Cube Worlds has been rebuilt twice. Each version taught us what the game wanted to be. + +## [v1 — The Commands Era](/history/v1/) +A commands-only Telegram bot. Dice, daily claims, referrals — all typed in chat. + +## [v2 — The Mini App (Ancient Worlds)](/history/v2/) +The current game: a full NFT-gated ARPG Mini App with castles, heroes, PvP, a weekly boss, and real USDT tournaments. $CUBE became DB-only. + +## [v3 — The React Rebuild](/history/v3/) +In progress. A ground-up React rebuild with a completely new design language. diff --git a/src/landing/src/content/history/v1.md b/src/landing/src/content/history/v1.md new file mode 100644 index 0000000..1343ee0 --- /dev/null +++ b/src/landing/src/content/history/v1.md @@ -0,0 +1,38 @@ +--- +title: v1 — The Commands Era — Cube Worlds +description: Cube Worlds v1 was a commands-only Telegram bot — dice rolls, daily claims, referrals, and on-chain $CUBE, all typed in chat. +--- +# v1 — The Commands Era + +**Git anchor:** tag `v1` → commit `d90e540` — the last commit before the pivot to the Mini App. + +Cube Worlds started as a pure Telegram bot. No webview, no wallet UI — just you, the bot, and a handful of slash commands. + +## What it did + +- `/dice` — roll for $CUBE tokens. Win or lose, the chain recorded it. +- `/claim` — daily reward with a streak multiplier. +- `/ref` — referral links that credited both sides in $CUBE. +- $CUBE was a real on-chain jetton on TON. Every dice win was a blockchain transaction. + +## The experience + +> **You:** `/dice` +> +> **CubeBot:** 🎲 You rolled a 5! You win **120 $CUBE**. Balance: 840 $CUBE. + +> **You:** `/claim` +> +> **CubeBot:** ✅ Daily claim! +100 $CUBE (streak day 3 — 1.3× multiplier). Balance: 940 $CUBE. + +Simple, frictionless, and entirely chat-native. No install required — if you had Telegram, you could play. + +## What we learned + +The on-chain model hit gas and latency walls quickly. Every dice roll needed a real transaction — slow confirmations, wallet pop-ups, and rising fees made casual play frustrating. The gameplay loop was too thin to retain players past week one. + +v1 proved that Telegram users *would* engage with a crypto game, but the command-line interface and on-chain jetton were the wrong shape for retention. + +--- + +*Next: [v2 — The Mini App (Ancient Worlds)](/history/v2/)* diff --git a/src/landing/src/content/history/v2.md b/src/landing/src/content/history/v2.md new file mode 100644 index 0000000..bb12388 --- /dev/null +++ b/src/landing/src/content/history/v2.md @@ -0,0 +1,42 @@ +--- +title: v2 — The Mini App (Ancient Worlds) — Cube Worlds +description: Cube Worlds v2 is the current game — a full NFT-gated ARPG Mini App with castles, heroes, PvP, a weekly boss, and real USDT tournaments. +--- +# v2 — The Mini App (Ancient Worlds) + +**Git anchor:** tag `v2` → commit `5a047a6` — the current production build. + +v2 is a ground-up rebuild: a Telegram Mini App (Vue 3 + Fastify + MongoDB) with a full ARPG loop, real money rails, and a player economy anchored by NFT ownership. + +## The pivot that changed everything + +The first big decision was making $CUBE **DB-only**. No on-chain jetton, no gas fees, no wallet pop-ups for every action. The token is a number in a database — fast, free, and friction-free for players. The blockchain is reserved for what it does well: **NFT ownership**. + +To enter the game at all, you must own a Cube Worlds NFT. Minting one requires earning enough $CUBE votes to clear the eligibility floor, then passing an admin review. This gate keeps the player base invested from day one. + +## The game loop + +**Castles** are home base. Each castle has four upgrade tracks — Walls, Forge, Tavern, and Mine — that produce resources on an 8-hour production clock. The Mine scales $CUBE output; the Tavern sets how many heroes you can recruit. + +**Heroes** are the action layer. Recruit a knight, mage, archer, or rogue from the Tavern; run the daily dungeon for XP and loot; equip gear earned from the weekly boss. Each hero has a class, level, and stats that feed into the combat resolver. + +**PvP** runs on an ELO ladder. Arena fights burn a small $CUBE entry fee; Raid attacks stake 50 $CUBE and plunder 10% of the defender's resources on a win. Successful raids plant a shield on the defender for 8 hours. + +**The weekly boss** is a shared PvE event. Every player sends their best hero against the same seeded enemy; damage contributions are ranked at week's end, and the top contributors earn tiered equipment drops — legendary for the top 5%, epic for the top 20%, rare for the top 50%. + +**Tournaments** pay real USDT. A 20% pool accrues from all in-game revenue. At the end of each week, the pool is split among the top expedition earners by USDT weight — paid directly via xRocket transfer to their bound TON wallet. + +## Money rails + +- **TON donations** credit $CUBE votes, which drive NFT tier and mint queue position. +- **xRocket USDT** handles deposits, energy purchases, withdrawals, and tournament payouts — all stored as bigint micro-USDT, never a float. +- **Telegram Stars** power Season Pass subscriptions: an active pass raises the energy cap and waives tournament entry fees. +- **Adsgram** rewarded ads grant energy with per-day caps and HMAC nonce integrity. + +## See it in action + +Screenshots and phone-frame walkthroughs are on the [Screens page](/screens/). + +--- + +*Next: [v3 — The React Rebuild](/history/v3/)* diff --git a/src/landing/src/content/history/v3.md b/src/landing/src/content/history/v3.md new file mode 100644 index 0000000..865abd4 --- /dev/null +++ b/src/landing/src/content/history/v3.md @@ -0,0 +1,36 @@ +--- +title: v3 — The React Rebuild — Cube Worlds +description: Cube Worlds v3 is a ground-up React SPA rebuild with a new design direction — currently in progress. +--- +# v3 — The React Rebuild + +> 🚧 **In progress.** v3 is under active development. The game (v2) is live and fully playable while the rebuild happens alongside it. + +## The vision + +v2 proved the game loop. v3 is about making it feel like the game it wants to be. + +The rebuild uses **React** — consistent with the author's other projects and better suited to the component-heavy UI that the ARPG depth now demands. Vue 3 served v2 well, but the design system is hitting its limits as features compound. + +## What changes in v3 + +- **New design language** — a darker, more atmospheric visual style matching the Ancient Worlds theme. Less "mini app", more ARPG client. +- **React SPA** — full client-side routing, shared component library, and a tighter dev loop between the landing site and the game shell. +- **Richer combat UI** — round-by-round replays with animation beats instead of a flat result screen. +- **Clan layer** — the social infrastructure (clans, trading post, shared castle upgrades) that v2 deferred lands in v3. +- **On-chain equipment trading** — the Tact escrow contract and equipment NFT transfer, gated by Phase C/D milestones. + +## Roadmap + +- Migrate core screens (Castle, Heroes, Arena) to React shell +- Ship new design system tokens and Figma component library +- Add clan creation and clan-level castle upgrades +- Equipment trading post with 5% $CUBE burn +- Tact escrow contract audit and deploy +- Stars/gacha hero purchase flow (Phase D) + +## Get involved + +If you are a designer, React engineer, or game economy nerd who wants to help shape v3, reach out. + +Investor and partnership enquiries: [investors page](/investors/) or [t.me/babin](https://t.me/babin). diff --git a/src/landing/src/content/legal/privacy.md b/src/landing/src/content/legal/privacy.md new file mode 100644 index 0000000..c1bef2b --- /dev/null +++ b/src/landing/src/content/legal/privacy.md @@ -0,0 +1,59 @@ +--- +title: Privacy Policy — Cube Worlds +description: Privacy policy for Cube Worlds — what data we collect, how it is used, and how to contact us. +--- + +# Privacy Policy + +_Last updated: 2026-07-06_ + +## Who we are + +Cube Worlds is a Telegram Mini App game operated by Vladimir Babin ("we", "us", "our"). The game runs at **@cube_worlds_bot** on Telegram and is accessible via [cubeworlds.club](https://cubeworlds.club). + +## Data we collect + +When you play Cube Worlds, we collect and store the following information: + +- **Telegram identity.** Your Telegram user ID and username, passed by the Telegram Mini App API when you open the bot. We do not receive your phone number or email address from Telegram. +- **TON wallet address.** If you choose to connect a TON wallet (required for NFT minting and donation crediting), we store the wallet address after you supply a cryptographic proof of ownership (ton_proof). You are never required to share a seed phrase or private key. +- **Gameplay and ledger records.** Your in-game state — $CUBE balance, resource balances, NFT status, hero levels, expedition history, dungeon runs, arena matches, tournament entries, and similar gameplay data — is stored in our database to operate the game. +- **Transaction records.** USDT deposit and withdrawal records processed through the xRocket payment rail, Telegram Stars Season Pass charges, and TON donation amounts credited to your account. +- **Technical logs.** Standard server request logs (IP address, timestamp, endpoint) retained for up to 30 days for security and debugging. + +## How we use your data + +We use the data described above solely to: + +- Operate and personalise your Cube Worlds gameplay experience. +- Process payments and prize payouts. +- Prevent abuse, fraud, and cheating. +- Comply with legal obligations if required. + +## Data sharing + +We do **not** sell, rent, or trade your personal data to third parties. We share data only as needed to operate the game: + +- **xRocket** (Telegram payment processor) — for USDT deposit/withdrawal processing. +- **Adsgram** — receives only an anonymous reward confirmation when you view a rewarded ad; no personal identifiers are shared. +- **Stability AI / OpenAI** — your NFT generation request is sent to generate pixel art and a description. No personally identifying information is included in these requests beyond the request itself. + +## Data retention + +We retain your game account data for as long as your account is active. If you wish to have your data deleted, contact us at @babin on Telegram. + +## Security + +Game state is stored in a hosted MongoDB database with access controls. Wallet binding uses cryptographic proof (Ed25519 signature over a stateless HMAC nonce) to verify wallet ownership. We do not store private keys or seed phrases. + +## Children + +Cube Worlds is not directed at children under 13. If you believe a child has provided personal information, contact us at @babin and we will delete it. + +## Changes + +We may update this policy. Material changes will be announced via @cube_worlds_bot. Continued use of the game after a change constitutes acceptance. + +## Contact + +Questions about this policy: **@babin** on Telegram. diff --git a/src/landing/src/content/legal/terms.md b/src/landing/src/content/legal/terms.md new file mode 100644 index 0000000..6d629b3 --- /dev/null +++ b/src/landing/src/content/legal/terms.md @@ -0,0 +1,63 @@ +--- +title: Terms of Service — Cube Worlds +description: Terms of service for Cube Worlds — an entertainment game on Telegram and the TON blockchain. +--- + +# Terms of Service + +_Last updated: 2026-07-06_ + +## 1. Acceptance + +By accessing or playing Cube Worlds via **@cube_worlds_bot** on Telegram or through [cubeworlds.club](https://cubeworlds.club), you agree to these Terms of Service. If you do not agree, do not use the game. + +## 2. Entertainment product + +Cube Worlds is an **entertainment product** — a video game. Nothing on this site or in the game constitutes financial advice, investment advice, or an offer of securities. + +## 3. $CUBE is not a security or tradable asset + +**$CUBE is a DB-only in-game currency** measured in "votes." It exists solely within the Cube Worlds game database to gate NFT minting eligibility and power in-game economy mechanics. + +- $CUBE has no on-chain representation, no market price, and no exchange listing. +- $CUBE cannot be withdrawn, bridged, or transferred outside the game. +- $CUBE has no guaranteed monetary value. +- Purchasing energy packs, a Season Pass, or making TON donations may earn $CUBE as a gameplay mechanic — these are entertainment purchases, not investments. + +## 4. NFTs + +Cube Worlds NFTs (Mint, Castle, Hero, Equipment collections) are on-chain TON assets that gate access to game features. Owning an NFT grants game access as described in the game itself. NFTs are **not** investments and carry no promise of financial return. + +## 5. USDT prizes + +Weekly tournament prize pools are funded by a portion of in-game revenue and distributed via the xRocket payment rail. Prize amounts depend on revenue collected and the number of participants in a given week. We do not guarantee any specific prize amount. USDT payments are processed by xRocket; their terms apply to withdrawals. + +## 6. No warranties + +THE GAME IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. WE DO NOT WARRANT THAT THE GAME WILL BE UNINTERRUPTED, ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. + +## 7. Limitation of liability + +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, WE SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OF THE GAME. + +## 8. Prohibited conduct + +You agree not to: + +- Exploit bugs or use automation to gain unfair advantage. +- Attempt to reverse-engineer, scrape, or attack the game infrastructure. +- Use the game for money laundering or any unlawful purpose. + +Violation may result in account termination without refund. + +## 9. Changes to the game + +We may modify, suspend, or discontinue any feature of the game at any time. We will announce material changes via @cube_worlds_bot where possible. + +## 10. Governing law and disputes + +These terms are governed by applicable law. Disputes arising from use of Cube Worlds shall be subject to the applicable law of the jurisdiction of the operator. We will make reasonable efforts to resolve disputes amicably before any formal proceedings. + +## 11. Contact + +Questions about these terms: **@babin** on Telegram. diff --git a/src/landing/src/data/site.json b/src/landing/src/data/site.json new file mode 100644 index 0000000..556c441 --- /dev/null +++ b/src/landing/src/data/site.json @@ -0,0 +1,27 @@ +{ + "brand": "Cube Worlds", + "telegramBot": "https://t.me/cube_worlds_bot", + "contact": "https://t.me/babin", + "baseUrl": "https://cubeworlds.club", + "nav": [ + { "slug": "", "label": "Home" }, + { "slug": "gameplay", "label": "Gameplay" }, + { "slug": "economy", "label": "Economy" }, + { "slug": "screens", "label": "Screens" }, + { "slug": "history", "label": "History" }, + { "slug": "investors", "label": "Investors" }, + { "slug": "blog", "label": "Blog" }, + { "slug": "faq", "label": "FAQ" } + ], + "footer": [ + { "slug": "press", "label": "Press" }, + { "slug": "privacy", "label": "Privacy" }, + { "slug": "terms", "label": "Terms" } + ], + "metricsFallback": { + "players": "4,000+", + "minted": "1,200+", + "paidOut": "$1,400+", + "activeWeek": "800+" + } +} diff --git a/src/landing/src/layout.html b/src/landing/src/layout.html new file mode 100644 index 0000000..1c32e89 --- /dev/null +++ b/src/landing/src/layout.html @@ -0,0 +1,24 @@ + + + + + +{{title}} + + + + + + + + +{{extraCss}} + + +
+
+{{> nav}} +{{content}} +{{> footer}} + + diff --git a/src/landing/src/pages/economy.html b/src/landing/src/pages/economy.html new file mode 100644 index 0000000..b173526 --- /dev/null +++ b/src/landing/src/pages/economy.html @@ -0,0 +1,44 @@ +--- +title: Economy — Cube Worlds +description: DB-only $CUBE, USDT tournament prize pools, Season Pass, rewarded ads, and the 20% rewards pool — designed for the long game. +active: economy +--- +
+
+
+
Economy & Sustainability
+

Designed for the long game

+

A deliberately conservative token model: sinks before faucets, no premature listing, and a rewards pool funded by real revenue.

+
+
+
+

The $CUBE model

+
    +
  • $CUBE is DB-only. Votes come from daily claims, referrals and TON donations — no on-chain jetton, no premature TGE.
  • +
  • 🎟️NFT-gated. Owning a Cube Worlds NFT gates game entry. An escalating mint floor keeps supply honest as the collection grows.
  • +
  • 🔥Sinks before faucets. Energy refills, weight-boosts, hero recruitment, castle upgrades and tournament entry all burn $CUBE before any faucet turns on.
  • +
  • 💰20% rewards pool. A fifth of net revenue accrues to an append-only, idempotent rewards ledger in micro-USDT that funds player payouts.
  • +
  • 🏆Skill pays. Weekly tournaments rank by in-window expedition CUBE and pay winners real USDT via free xRocket transfers.
  • +
+
+
+
+
TON
Settlement chain
+
20%
Revenue → rewards pool
+
4
NFT collections
+
100%
In-Telegram, no download
+
+
+

Monetization rails

+
+
xRocket USDT deposits, withdrawals & payouts
+
Telegram Stars Season Pass
+
Adsgram rewarded ads → energy
+
TON donations → mint votes
+
+

All USDT is held as bigint micro-USDT with an overdraft-proof ledger and an hourly custody reconciliation guard.

+
+
+
+
+
diff --git a/src/landing/src/pages/faq.html b/src/landing/src/pages/faq.html new file mode 100644 index 0000000..7789a03 --- /dev/null +++ b/src/landing/src/pages/faq.html @@ -0,0 +1,21 @@ +--- +title: FAQ — Cube Worlds +description: How to mint, how prizes are paid, what's on-chain, and everything else you want to know about Cube Worlds. +active: faq +--- +
+
+
+
Questions
+

Good to know

+
+
+
Where do I actually play?

Entirely inside Telegram. This page is an information hub for players and investors — the game runs as a Mini App in @cube_worlds_bot. There's nothing playable on this website by design.

+
Is $CUBE a tradable token?

No. $CUBE is a DB-only in-game currency measured in "votes." It drives NFT mint eligibility and in-game economy sinks. There is no on-chain jetton and no premature token listing.

+
Why do I need an NFT to play?

Owning a Cube Worlds NFT gates entry to the game. You generate a pixel-art NFT in-app and clear an escalating mint floor with votes earned from daily claims, referrals and TON donations.

+
How are USDT prizes paid?

Weekly tournaments pay real USDT via the xRocket rail. The prize pool is funded by a rewards pool that accrues 20% of net revenue, tracked in an idempotent, overdraft-proof micro-USDT ledger.

+
What can I spend money on?

Optional: energy packs and a Telegram Stars Season Pass. You can also watch rewarded ads for free energy. Everything is designed so sinks exist before any token faucet is enabled.

+
What chain is this on?

The Open Network (TON). NFTs, wallet binding (ton_proof), and donations are on-chain; game state is DB-canonical and on-chain-ready for the collections that are deployed.

+
+
+
diff --git a/src/landing/src/pages/gameplay.html b/src/landing/src/pages/gameplay.html new file mode 100644 index 0000000..902b756 --- /dev/null +++ b/src/landing/src/pages/gameplay.html @@ -0,0 +1,39 @@ +--- +title: Gameplay — Cube Worlds +description: Castles, heroes, dungeons, quests, arena, raids, and the weekly boss — the full ARPG loop inside Telegram on TON. +active: gameplay +--- +
+
+
+
The Game
+

A full ARPG loop, in chat

+

Every system is on-chain-ready and DB-canonical. Progress compounds while you're away and pays off when you return.

+
+
+
🏰

Your Castle

Four upgrade tracks — Mine, Walls, Forge, Tavern. Resources accrue on an 8-hour production tick. Founders earn +20%.

+
🛡️

Heroes & Dungeons

Recruit knights, mages, archers and rogues at the Tavern. Run the deterministic daily dungeon and 8-hour quests for XP and loot.

+
⚔️

Equipment

Four slots × four rarities. Gear folds directly into combat. Rare drops from quests and the weekly boss.

+
🗡️

Arena & Raids

Async-snapshot PvP on a single ELO ladder. Raid rival castles to plunder resources — or shield up after you're hit.

+
🐉

Weekly Boss

The whole server chips away at a shared boss. Top the damage board for legendary, epic and rare equipment tiers.

+
⚔️

Expeditions

Spend energy to dispatch expeditions across five cube-worlds. A congestion model dilutes crowded worlds — pick your risk.

+
🏅

Tournaments

Monday-aligned weekly tournaments with real USDT prize pools, funded by the rewards pool and paid out via xRocket.

+
🎟️

NFT-gated entry

Generate a pixel-art NFT, climb the mint queue with votes, and own your seat in the world before you play.

+
+
+
+ +
+
+
+
Getting Started
+

Three steps to your empire

+

No app store, no seed phrase gymnastics. Open the bot and you're in.

+
+
+
1

Open in Telegram

Launch @cube_worlds_bot and tap Play. The Mini App opens right in your chat — nothing to install.

+
2

Connect & mint

Bind your TON wallet, generate your pixel-art NFT, and earn votes from donations to clear the mint floor.

+
3

Play & earn

Build your castle, recruit heroes, raid rivals, and climb the weekly tournament for real USDT payouts.

+
+
+
diff --git a/src/landing/src/pages/home.html b/src/landing/src/pages/home.html new file mode 100644 index 0000000..b8cb11a --- /dev/null +++ b/src/landing/src/pages/home.html @@ -0,0 +1,116 @@ +--- +title: Cube Worlds — Ancient Worlds ARPG on TON +description: An NFT-gated ancient-worlds ARPG inside Telegram on TON. Castles, heroes, PvP, weekly boss, USDT tournaments. +active: +--- +
+
+ ⛓️ Built on TON · 🎮 Playable inside Telegram +

Build an empire in the Ancient Worlds

+

+ Cube Worlds is an NFT-gated idle-ARPG that lives entirely inside Telegram. Raise a castle, + recruit heroes, raid rivals, hunt the weekly boss, and compete for real USDT prize pools — + all powered by the $CUBE economy on TON. +

+ +

Own a Cube Worlds NFT to enter · Free to start · No download

+ +
+
🏰 Castles & production
+
🛡️ Heroes & PvE dungeons
+
🗡️ Arena & raids
+
🐉 Weekly boss
+
🏅 USDT tournaments
+
+ +
+
4,000+
Players
+
1,200+
NFTs minted
+
$1,400+
USDT paid out
+
800+
Active this week
+
+
+
+ +
+ +
+ +
+
+
+

Ready to raise your castle?

+

Join the Ancient Worlds. Mint your NFT, gather your heroes, and compete for the weekly USDT pool.

+ ✈️ Open Cube Worlds in Telegram +
+
+
+ + diff --git a/src/landing/src/pages/investors.html b/src/landing/src/pages/investors.html new file mode 100644 index 0000000..0b61cd5 --- /dev/null +++ b/src/landing/src/pages/investors.html @@ -0,0 +1,169 @@ +--- +title: Investors & Partners — Cube Worlds +description: Traction, monetization model, NFT partnership tracks, B2B dev services, and contact for potential investors and collaborators. +active: investors +--- +
+
+
+
Traction
+

Real numbers, real revenue

+

Cube Worlds is live, monetised, and growing — entirely inside Telegram, with no app store dependency.

+
+
+
4,000+
Players
+
1,200+
NFTs minted
+
$1,400+
USDT paid out
+
800+
Active this week
+
+
+

Economy model in brief

+
    +
  • DB-only $CUBE. No premature token listing. Sinks (castle upgrades, hero recruitment, tournament entry, energy refills) are live before any faucet opens.
  • +
  • 💰20% rewards pool. One fifth of net revenue goes to an append-only micro-USDT ledger. Weekly tournaments pay real USDT via xRocket.
  • +
  • 🎟️NFT-gated entry. Four on-chain TON collections — mint, castle, hero, equipment. An escalating mint floor keeps supply honest as the player base grows.
  • +
  • 🏦Multiple revenue rails. xRocket USDT (energy packs), Telegram Stars (Season Pass), Adsgram rewarded ads, and TON donations feed the rewards pool.
  • +
+

Full token model and sink/faucet design: Economy page →

+
+
+
+ +
+
+
+
Investment / Fundraising
+

The opportunity

+

We are raising a seed round to accelerate growth, deepen the game loop, and expand to additional chains and distribution channels.

+
+
+
+ 💵 +

The ask

+

Pre-seed / seed funding to hire two additional full-stack engineers, scale paid-user-acquisition across Telegram, and complete Phase D on-chain integrations (trading post, equipment bridge, clan treasury). Equity or SAFE structures considered.

+
+
+ 🗺️ +

Roadmap highlights

+

Phase A–C shipped: castles, heroes, PvP arena, raids, weekly boss, equipment NFTs. Phase D: clan system, equipment trading post (5% CUBE burn), Stars-gated hero gacha, on-chain equipment transfer. Full milestone history at v3 release notes.

+
+
+ +

Why now

+

TON Mini Apps crossed 1 B users in Telegram. First-mover advantage in idle-ARPG on TON is open. The game is live, the money rails are wired, and the four NFT collections are on-chain — capital now compounds on an already-working product.

+
+
+ 📈 +

Unit economics

+

20% of gross revenue is committed to the player rewards pool, creating a retention moat. Season Pass LTV, ad CPM, and energy pack average order values are benchmarked and documented. See the economy page for the full breakdown.

+
+
+
+
+ +
+
+
+
NFT Collabs & Partnerships
+

Four live collections — four collab tracks

+

Each Cube Worlds NFT collection is a standalone on-chain TON asset. Partner collections get a dedicated integration, shared drops, and mutual audience exposure.

+
+
+
+ 🎨 +

Mint collection

+

Pixel-art access passes — the entry gate to the game. A partner can contribute art styles to the generator, co-brand a limited drop, and earn referral credit for every wallet that mints through their link.

+
+
+ 🏰 +

Castle collection

+

Castle NFTs are generated when players reach milestone upgrade levels. Partner branding can appear as castle skin variants for holders of a partner collection, minted automatically on wallet proof.

+
+
+ 🛡️ +

Hero collection

+

First hero of each class is soulbound to the player. A partner can sponsor a named hero class — exclusive art, a custom founder variant, and a stat modifier applied to all holders of the partner NFT.

+
+
+ ⚔️ +

Equipment collection

+

Equipment NFTs drop from quests and boss events. Partner collections can fund a limited equipment set (weapon + armour) as a co-branded seasonal drop, distributed to both communities simultaneously.

+
+
+ 🤝 +

What a collab looks like

+

Standard integration: cross-promo (mutual Telegram channel announcements + in-game news banner), shared drop (a limited NFT batch minted to both communities on a fixed date), and technical integration (wallet-proof gating so partner-NFT holders get in-game perks without any action on their end). We handle the on-chain mechanics; partners bring the audience.

+
+
+
+
+ +
+
+
+
B2B / Dev Services
+

Built by a team that ships

+

Cube Worlds is a solo-founder project. Every system described on this site was designed, built, tested, and deployed by one engineer.

+
+
+
+ 👤 +

Vladimir Babin — founder & lead engineer

+

Full-stack TypeScript (Node, Vue 3, Fastify). TON ecosystem specialist: on-chain NFT deployment, ton_proof wallet binding, TonConnect UI, and TON watcher integrations. Available for consulting, architecture review, and contract delivery on TON Mini App or game projects.

+
+
+ ⛓️ +

TON chain integration

+

Wallet binding via ton_proof with a stateless HMAC nonce, on-chain NFT mint (NftItem deploy), TON watcher for donation crediting. Reusable as a standalone integration module for any Telegram Mini App that needs verified wallet ownership.

+
+
+ 💸 +

xRocket idempotent micro-USDT ledger

+

Append-only USDT ledger in bigint micro-USDT with overdraft-proof CAS debits, E11000-idempotent deposit webhooks, hourly custody reconciliation, and auto-pause on divergence. Drop-in for any TON Mini App needing a production-grade USDT money rail.

+
+
+ ⚔️ +

Deterministic combat engine

+

Seeded mulberry32 RNG, stat-based round resolver, equipment bonuses, 1000×-reproducible outcomes. Fully unit-tested with Node.js built-in test runner. Suitable for any game needing provably fair, replay-verifiable PvP.

+
+
+ ⚙️ +

Settlement workers

+

Idempotent, crash-safe workers for expedition payouts, weekly tournament settlement, boss-week reward distribution, and NFT mint runners — all pure-handler + composer pattern, independently testable and deployable without config coupling.

+
+
+ 🧪 +

Production-grade test coverage

+

748 tests across 115 files. Handler DI pattern allows full test isolation without config coupling. Every money path, CAS operation, and settlement worker is covered. The full test suite runs in under 30 seconds.

+
+
+
+
+ +
+
+
+

Let's talk

+

Interested in investing, partnering on an NFT collab, or contracting dev work on TON? Reach out directly.

+ ✈️ Message @babin on Telegram +
+
+
+ + diff --git a/src/landing/src/pages/press.html b/src/landing/src/pages/press.html new file mode 100644 index 0000000..c3da56f --- /dev/null +++ b/src/landing/src/pages/press.html @@ -0,0 +1,105 @@ +--- +title: Press & Media Kit — Cube Worlds +description: Media kit, brand assets, key facts, and contact for journalists and content creators covering Cube Worlds. +active: +--- +
+
+
+
Media Kit
+

Cube Worlds — Press

+

+ Cube Worlds is an NFT-gated idle-ARPG that runs entirely inside Telegram on the TON blockchain. + Raise a castle, recruit heroes, fight a weekly boss, raid rivals, and compete for real USDT prize pools — + all driven by a transparent DB-only $CUBE economy with no premature token listing. +

+
+ +
+
+ 📋 +

Key facts

+
    +
  • Chain: The Open Network (TON)
  • +
  • Format: Telegram Mini App
  • +
  • Economy: DB-only $CUBE, real USDT prizes via xRocket
  • +
  • NFT collections: Mint · Castle · Hero · Equipment (all on-chain)
  • +
  • Entry: NFT-gated; escalating mint floor
  • +
  • Revenue rails: xRocket USDT energy packs, Telegram Stars Season Pass, rewarded ads
  • +
  • Press contact: @babin on Telegram
  • +
+
+ +
+ 🎨 +

Brand palette

+

The canonical Cube Worlds palette. Use these values on white or dark backgrounds only.

+
+
+ +
Accent Orange
#ff9933
+
+
+ +
Accent Purple
#6633ff
+
+
+ +
Background
#000018
+
+
+
+ +
+ 🖼️ +

Brand assets

+

Download the Cube Worlds logo and browse in-game screenshots for editorial use.

+ +

+ Logo and screenshots may be used in editorial coverage of Cube Worlds. Do not alter the logo colours or proportions. + Contact @babin for hi-res exports or specific scene requests. +

+
+ +
+ 📊 +

Traction snapshot

+
    +
  • Players: 4,000+
  • +
  • NFTs minted: 1,200+
  • +
  • USDT paid out: $1,400+
  • +
  • Active this week: 800+
  • +
+

+ Numbers update live from the game database. For audited figures, contact @babin. +

+
+ +
+ 📰 +

One-paragraph boilerplate

+

+ Cube Worlds is an NFT-gated idle-ARPG built as a Telegram Mini App on The Open Network (TON). + Players raise castles, recruit heroes, hunt weekly bosses, and compete in arena PvP — + all inside Telegram with no download required. Progression is powered by $CUBE, a DB-only + in-game currency that gates NFT minting through an escalating floor mechanism. + Real USDT prize pools are distributed weekly via xRocket, funded by a transparent 20% + revenue rewards pool. Cube Worlds is developed and operated by Vladimir Babin. +

+
+ +
+ ✈️ +

Contact

+

+ For interview requests, review access, partnership enquiries, or any press materials + not listed here, reach out directly. +

+ Message @babin on Telegram +
+
+
+
diff --git a/src/landing/src/pages/screens.html b/src/landing/src/pages/screens.html new file mode 100644 index 0000000..f4af35f --- /dev/null +++ b/src/landing/src/pages/screens.html @@ -0,0 +1,280 @@ +--- +title: Screens — Cube Worlds +description: A faithful tour of every Cube Worlds Mini App screen. +active: screens +extraCss: app +--- + +
+
+
+
Product Tour
+

Every screen, as players see it

+

These are faithful mockups of the live Telegram Mini App — the same cosmic UI, balance bar and emoji navigation. The game itself runs only inside Telegram.

+
+ +
+ + + + + +
+ + +
+
+ + diff --git a/src/landing/src/partials/footer.html b/src/landing/src/partials/footer.html new file mode 100644 index 0000000..51c36df --- /dev/null +++ b/src/landing/src/partials/footer.html @@ -0,0 +1,14 @@ +
+
+
Cube Worlds
+
+ Telegram + {{footerLinks}} +
+
+

+ Cube Worlds is a game on The Open Network (TON). $CUBE is an in-game, database-only currency and is not a + financial instrument, security, or tradable token. NFTs and in-game items are for entertainment. Nothing on this + page is investment advice. Play responsibly. +

+
diff --git a/src/landing/src/partials/nav.html b/src/landing/src/partials/nav.html new file mode 100644 index 0000000..8afe4eb --- /dev/null +++ b/src/landing/src/partials/nav.html @@ -0,0 +1,10 @@ + diff --git a/src/landing/src/styles/app-preview.css b/src/landing/src/styles/app-preview.css new file mode 100644 index 0000000..d1eaa20 --- /dev/null +++ b/src/landing/src/styles/app-preview.css @@ -0,0 +1,108 @@ +/* Gallery filter */ +.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-bottom: 2.25rem; } +.filter-btn { background: var(--panel); border: 1px solid var(--border); color: var(--muted); border-radius: 999px; + padding: 0.45rem 1rem; font-size: 0.85rem; cursor: pointer; font-weight: 600; transition: all .15s; } +.filter-btn:hover { color: #fff; } +.filter-btn.active { background: linear-gradient(135deg, var(--accent), #ff7a00); color: #201200; border-color: transparent; } + +.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 2rem; justify-items: center; } +.screen-wrap { display: flex; flex-direction: column; align-items: center; } +.screen-wrap.hidden { display: none; } +.screen-label { margin-bottom: 0.85rem; text-align: center; max-width: 300px; } +.screen-label .name { font-weight: 700; font-size: 1rem; } +.screen-label .route { color: var(--dim); font-size: 0.75rem; font-family: ui-monospace, monospace; } +.screen-label .desc { color: var(--muted); font-size: 0.78rem; margin-top: 0.25rem; } +.state-tag { display:inline-block; margin-top:0.35rem; font-size:0.68rem; font-weight:700; letter-spacing:0.3px; + padding:0.12rem 0.5rem; border-radius:6px; background: rgba(102,51,255,0.18); color:#c9b8ff; border:1px solid rgba(102,51,255,0.35); } + +/* Phone frame + in-app theme (mirrors the real Mini App) */ +.phone { + width: 300px; height: 600px; border-radius: 30px; border: 9px solid #111436; + box-shadow: 0 22px 50px rgba(0,0,0,0.6); overflow: hidden; position: relative; background: #000033; +} +.cosmos { position: absolute; inset: 0; display: flex; flex-direction: column; + background: radial-gradient(circle at 30% 12%, #14235e 0%, #000033 55%); overflow: hidden; } +.stars-lite::before { content:''; position:absolute; inset:0; + background-image: + radial-gradient(1px 1px at 20% 30%, #fff, transparent), + radial-gradient(1px 1px at 70% 20%, #fff, transparent), + radial-gradient(1px 1px at 40% 60%, #fff, transparent), + radial-gradient(1px 1px at 85% 75%, #fff, transparent), + radial-gradient(1px 1px at 55% 85%, #fff, transparent), + radial-gradient(1px 1px at 10% 80%, #fff, transparent); + opacity: 0.6; } +.mini-sun { position:absolute; top:8%; left:18%; width:52px; height:52px; border-radius:50%; + background: radial-gradient(circle,#ff9933 0%,#ff6600 100%); box-shadow:0 0 36px #ff9933,0 0 60px #ff6600; z-index:1; } +.top-bar { position:relative; z-index:10; display:flex; justify-content:space-between; align-items:center; + padding:0.5rem 0.8rem; height:42px; background:rgba(0,0,51,0.8); backdrop-filter:blur(5px); + border-bottom:1px solid rgba(255,255,255,0.1); } +.coin-balance { font-size:0.9rem; font-weight:bold; color:#fff; } +.ton-connect { background:#0098ea; color:#fff; font-size:0.7rem; font-weight:600; padding:0.28rem 0.55rem; border-radius:8px; } +.content { position:relative; z-index:10; flex:1; overflow-y:auto; padding:0.7rem; } +.content::-webkit-scrollbar { width:4px; } .content::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.15); border-radius:2px; } +.footer { position:relative; z-index:10; height:48px; display:flex; justify-content:space-around; align-items:center; + background:rgba(0,0,51,0.8); backdrop-filter:blur(5px); border-top:1px solid rgba(255,255,255,0.1); font-size:1.25rem; } +.footer a { text-decoration:none; opacity:0.8; position:relative; } +.footer a.active { opacity:1; } +.footer a.active::after { content:''; position:absolute; bottom:-5px; left:20%; width:60%; height:2px; background:var(--accent); } + +.app-card { background:rgba(0,0,51,0.7); border-radius:1rem; padding:0.9rem; border:1px solid rgba(255,255,255,0.1); + box-shadow:0 0 20px rgba(51,102,255,0.2); backdrop-filter:blur(5px); } +h1.a-title, h2.a-title { color:#fff; font-size:1.2rem; margin:0 0 0.85rem; text-align:center; text-shadow:0 0 8px rgba(255,255,255,0.5); } +.sub-card { background:rgba(255,255,255,0.04); border-radius:0.8rem; padding:0.7rem 0.8rem; margin-bottom:0.7rem; border:1px solid rgba(255,255,255,0.08); } +.card-title { color:#ccccff; font-size:0.8rem; margin-bottom:0.4rem; } +.a-stat-row { display:flex; justify-content:space-between; border-bottom:1px solid rgba(255,255,255,0.1); padding:0.22rem 0; font-size:0.82rem; } +.a-btn { background:var(--accent); color:#000; border:none; border-radius:8px; padding:0.5rem 0.85rem; font-weight:bold; cursor:pointer; width:100%; font-size:0.85rem; margin-top:0.45rem; } +.a-btn.blue { background:linear-gradient(135deg,#3366ff,#6633ff); color:#fff; } +.a-btn.raid { background:linear-gradient(135deg,#803030,#aa4040); color:#fff; } +.a-btn.secondary { background:rgba(255,255,255,0.15); color:#fff; } +.a-btn.ghost { background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.2); color:#fff; } +.a-btn.sm { width:auto; padding:0.38rem 0.7rem; font-size:0.78rem; margin:0; } +.a-btn:disabled { opacity:0.5; } +.a-ok { color:var(--ok); font-size:0.82rem; } .a-warn { color:var(--warn); font-size:0.82rem; } +.a-note { color:#aaaacc; font-size:0.75rem; } .a-info { color:#ccccff; font-size:0.82rem; text-align:center; padding:0.4rem 0; } +.a-lead { font-size:0.95rem; text-align:center; margin:0.3rem 0; } +.founder-badge { text-align:center; color:#ffd966; font-weight:700; margin-bottom:0.7rem; font-size:0.82rem; } +.rgrid { display:grid; grid-template-columns:repeat(4,1fr); gap:0.35rem; margin-bottom:0.7rem; } +.rc { background:rgba(255,255,255,0.05); border-radius:0.55rem; padding:0.4rem; text-align:center; } +.rc .l { display:block; color:#aaaacc; font-size:0.6rem; } .rc .v { display:block; color:#fff; font-weight:700; font-size:0.9rem; } +.track { display:flex; align-items:center; justify-content:space-between; gap:0.5rem; padding:0.5rem 0.7rem; border-radius:0.7rem; + border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.04); margin-bottom:0.45rem; } +.track .tn { color:#fff; font-weight:700; font-size:0.82rem; display:block; } .track .td { color:#aaaacc; font-size:0.66rem; } +.a-link { display:block; text-align:center; color:#aaddff; text-decoration:none; padding:0.45rem; font-size:0.82rem; } +.preview-image { width:100%; border-radius:8px; aspect-ratio:1; image-rendering:pixelated; display:flex; align-items:center; justify-content:center; font-size:2.4rem; + background: repeating-conic-gradient(#6b4c9a 0% 25%, #4a3270 0% 50%) 50% / 20px 20px; } +.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:0.35rem; } .grid4 { display:grid; grid-template-columns:repeat(4,1fr); gap:0.35rem; } +.ebar { height:0.65rem; background:rgba(255,255,255,0.1); border-radius:0.35rem; overflow:hidden; margin-top:0.3rem; } +.efill { height:100%; background:linear-gradient(90deg,#3366ff,#6633ff); } +.toggle { flex:1; padding:0.38rem; border-radius:0.55rem; border:1px solid rgba(255,255,255,0.2); background:rgba(255,255,255,0.05); color:#aaaacc; font-size:0.78rem; } +.toggle.active { background:linear-gradient(90deg,#3366ff,#6633ff); color:#fff; border-color:transparent; } +.world { padding:0.6rem 0.75rem; border-radius:0.75rem; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.04); margin-bottom:0.45rem; } +.world .wn { color:#fff; font-weight:700; margin-bottom:0.3rem; font-size:0.88rem; } .world .ws { display:flex; gap:0.8rem; margin-bottom:0.45rem; color:#ccccff; font-size:0.76rem; } +.herocard { padding:0.55rem 0.75rem; border-radius:0.75rem; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.04); margin-bottom:0.45rem; } +.hh { display:flex; justify-content:space-between; align-items:center; } .hn { color:#fff; font-weight:700; font-size:0.82rem; text-transform:capitalize; } .hx { color:#aaaacc; font-size:0.68rem; } +.chip-g { font-size:0.63rem; color:#dde; background:rgba(255,255,255,0.06); border-radius:0.5rem; padding:0.1rem 0.38rem; display:inline-block; margin:0.3rem 0.2rem 0 0; } +.chip-g.epic { color:#cc99ff; } .chip-g.legendary { color:#ffcc66; } .chip-g.rare { color:#99ccff; } +.hact { display:flex; gap:0.35rem; margin-top:0.45rem; } +.sel { background:rgba(0,0,40,0.8); color:#fff; border:1px solid rgba(255,255,255,0.15); border-radius:0.4rem; font-size:0.76rem; padding:0.32rem; flex:1; } +.boss-name { color:#ffcccc; font-size:1.05rem; font-weight:700; text-align:center; margin-bottom:0.3rem; } +.boss-stats { color:#ccccff; font-size:0.8rem; text-align:center; margin-bottom:0.4rem; } .board-line { color:#fff; font-size:0.8rem; text-align:center; } +.rounds { margin:0.4rem 0 0; padding-left:1rem; color:#ccccff; font-size:0.7rem; } .rounds .hp { color:#8888aa; } +.rhead { font-weight:700; margin-bottom:0.4rem; font-size:0.82rem; } .rhead.win { color:var(--ok); } .rhead.loss { color:#ff9999; } .rhead.dmg { color:#ffddaa; } +.rating { color:#aaddff; font-size:1.05rem; font-weight:700; text-align:center; } .record { color:#ccccff; font-size:0.8rem; text-align:center; } +.hrow { display:flex; gap:0.35rem; flex-wrap:wrap; padding:0.22rem 0; border-bottom:1px solid rgba(255,255,255,0.06); font-size:0.72rem; color:#ccccff; } +.won { color:var(--ok); } .lost { color:#ff9999; } .delta { color:#aaddff; } .lootc { color:#ffddaa; } +.prize-value { color:#fff; font-size:1.4rem; font-weight:700; text-align:center; display:block; text-shadow:0 0 8px rgba(102,51,255,0.4); } +.prize-label, .prize-sub { display:block; text-align:center; color:#ccccff; font-size:0.76rem; } +.brow { display:flex; align-items:center; gap:0.5rem; padding:0.38rem 0.1rem; border-bottom:1px solid rgba(255,255,255,0.08); font-size:0.8rem; } +.brank { width:2.2rem; color:#ffd479; font-weight:700; } .buser { flex:1; } .bscore { color:#fff; font-weight:600; } +.bal-value { color:#fff; font-size:1.3rem; font-weight:700; text-align:center; display:block; text-shadow:0 0 8px rgba(102,51,255,0.4); } +.field { padding:0.42rem 0.55rem; border-radius:0.55rem; border:1px solid rgba(255,255,255,0.2); background:rgba(255,255,255,0.05); color:#fff; font-size:0.8rem; flex:1; width:100%; } +.arow { display:flex; gap:0.35rem; margin-bottom:0.35rem; } +.lbrow { display:flex; align-items:center; gap:0.6rem; padding:0.4rem 0.25rem; border-bottom:1px solid rgba(255,255,255,0.06); font-size:0.8rem; } +.lbrank { width:2.5rem; font-weight:700; } .tier1 .lbrank { color:#ffd700; } .tier2 .lbrank { color:#c0c0c0; } .tier3 .lbrank { color:#cd7f32; } +.lbwallet { flex:1; font-family:ui-monospace,monospace; font-size:0.72rem; color:#dde; } .lbvotes { color:#fff; font-weight:600; } +.progress-bar { height:1.3rem; background:rgba(255,255,255,0.1); border-radius:0.65rem; position:relative; overflow:hidden; margin-bottom:0.5rem; } +.progress-fill { height:100%; background:linear-gradient(90deg,#3366ff,#6633ff); box-shadow:0 0 10px rgba(102,51,255,0.5); } +.ptext { color:#fff; font-size:0.88rem; font-weight:bold; text-align:center; } +.cnft-img { width:100%; aspect-ratio:1; border-radius:10px; background: repeating-conic-gradient(#3a5a9a 0% 25%, #27406e 0% 50%) 50% / 26px 26px; display:flex; align-items:center; justify-content:center; font-size:3rem; } diff --git a/src/landing/src/styles/site.css b/src/landing/src/styles/site.css new file mode 100644 index 0000000..15b8184 --- /dev/null +++ b/src/landing/src/styles/site.css @@ -0,0 +1,185 @@ +:root { + --bg: #000018; + --bg2: #05061e; + --panel: #0b0d28; + --panel2: #10133a; + --border: #1e224e; + --accent: #ff9933; + --accent2: #6633ff; + --blue: #3366ff; + --text: #e7ecff; + --muted: #9aa2cc; + --dim: #6a7099; + --ok: #66ffb3; + --warn: #ffcc66; + --err: #ff6b6b; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.55; + overflow-x: hidden; +} +a { color: inherit; } +.wrap { max-width: 1200px; margin: 0 auto; padding: 0 1.25rem; } +h1, h2, h3 { line-height: 1.15; } +.accent { color: var(--accent); } +.grad-text { + background: linear-gradient(100deg, #ff9933, #ffdd66 40%, #9d7bff 80%); + -webkit-background-clip: text; background-clip: text; color: transparent; +} + +/* Starfield backdrop */ +.cosmos-bg { + position: fixed; inset: 0; z-index: -2; + background: + radial-gradient(circle at 78% 8%, rgba(102,51,255,0.20), transparent 45%), + radial-gradient(circle at 12% 70%, rgba(255,153,51,0.12), transparent 42%), + linear-gradient(180deg, #000018, #05061e 60%, #02030f); +} +.cosmos-bg::before { + content: ''; position: absolute; inset: 0; + background-image: + radial-gradient(1px 1px at 15% 20%, #fff, transparent), + radial-gradient(1px 1px at 65% 12%, #fff, transparent), + radial-gradient(1px 1px at 42% 55%, #fff, transparent), + radial-gradient(1px 1px at 88% 68%, #fff, transparent), + radial-gradient(1px 1px at 30% 82%, #fff, transparent), + radial-gradient(2px 2px at 72% 40%, #fff, transparent), + radial-gradient(1px 1px at 8% 45%, #fff, transparent), + radial-gradient(1px 1px at 55% 92%, #fff, transparent); + opacity: 0.5; +} +.sun-glow { + position: fixed; top: -80px; right: 8%; z-index: -1; + width: 220px; height: 220px; border-radius: 50%; + background: radial-gradient(circle, #ff9933 0%, #ff6600 55%, transparent 72%); + filter: blur(6px); opacity: 0.55; +} + +/* Nav */ +nav { + position: sticky; top: 0; z-index: 50; + backdrop-filter: blur(10px); + background: rgba(4,5,20,0.72); + border-bottom: 1px solid var(--border); +} +nav .wrap { display: flex; align-items: center; justify-content: space-between; height: 62px; } +.brand { display: flex; align-items: center; gap: 0.55rem; font-weight: 800; font-size: 1.15rem; letter-spacing: 0.3px; } +.brand .logo { font-size: 1.4rem; } +.nav-links { display: flex; gap: 1.5rem; align-items: center; } +.nav-links a { text-decoration: none; color: var(--muted); font-size: 0.92rem; font-weight: 600; transition: color .2s; } +.nav-links a:hover { color: #fff; } +.btn { + display: inline-flex; align-items: center; gap: 0.5rem; + text-decoration: none; font-weight: 700; cursor: pointer; + border-radius: 12px; padding: 0.7rem 1.25rem; font-size: 0.95rem; border: none; + transition: transform .15s, box-shadow .2s; +} +.btn-primary { background: linear-gradient(135deg, #0098ea, #22a0ff); color: #fff; box-shadow: 0 6px 20px rgba(0,152,234,0.35); } +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 28px rgba(0,152,234,0.45); } +.btn-ghost { background: rgba(255,255,255,0.06); border: 1px solid var(--border); color: #fff; } +.btn-ghost:hover { background: rgba(255,255,255,0.11); } +.btn-accent { background: linear-gradient(135deg, #ff9933, #ff7a00); color: #201200; box-shadow: 0 6px 20px rgba(255,153,51,0.3); } +.btn-accent:hover { transform: translateY(-2px); } + +/* Hero */ +header.hero { padding: 4.5rem 0 3rem; text-align: center; } +.badge-pill { + display: inline-flex; align-items: center; gap: 0.5rem; + background: rgba(102,51,255,0.14); border: 1px solid rgba(102,51,255,0.4); + color: #c9b8ff; font-size: 0.8rem; font-weight: 600; + padding: 0.35rem 0.85rem; border-radius: 999px; margin-bottom: 1.5rem; +} +header.hero h1 { font-size: clamp(2.3rem, 6vw, 4rem); margin: 0 0 1rem; font-weight: 900; } +header.hero p.lead { font-size: clamp(1.05rem, 2.2vw, 1.3rem); color: var(--muted); max-width: 720px; margin: 0 auto 2rem; } +.hero-cta { display: flex; gap: 0.85rem; justify-content: center; flex-wrap: wrap; } +.hero-sub { margin-top: 1.25rem; color: var(--dim); font-size: 0.85rem; } + +.chips { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-top: 2.5rem; } +.chip { background: var(--panel); border: 1px solid var(--border); border-radius: 999px; padding: 0.45rem 1rem; font-size: 0.85rem; color: var(--muted); } +.chip b { color: #fff; } + +/* Sections */ +section { padding: 4rem 0; } +.section-head { text-align: center; max-width: 720px; margin: 0 auto 3rem; } +.section-head .eyebrow { color: var(--accent); font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; font-size: 0.78rem; } +.section-head h2 { font-size: clamp(1.8rem, 4vw, 2.6rem); margin: 0.6rem 0 0.75rem; font-weight: 800; } +.section-head p { color: var(--muted); font-size: 1.05rem; margin: 0; } + +/* Feature grid */ +.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.1rem; } +.feature { + background: linear-gradient(180deg, var(--panel), var(--bg2)); + border: 1px solid var(--border); border-radius: 18px; padding: 1.5rem; + transition: transform .2s, border-color .2s; +} +.feature:hover { transform: translateY(-4px); border-color: rgba(255,153,51,0.4); } +.feature .ico { font-size: 2rem; margin-bottom: 0.75rem; display: block; } +.feature h3 { margin: 0 0 0.5rem; font-size: 1.15rem; } +.feature p { margin: 0; color: var(--muted); font-size: 0.92rem; } + +/* Economy / investor */ +.econ-grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 2rem; align-items: start; } +@media (max-width: 860px){ .econ-grid { grid-template-columns: 1fr; } } +.econ-card { background: linear-gradient(180deg, var(--panel2), var(--panel)); border: 1px solid var(--border); border-radius: 18px; padding: 1.75rem; } +.econ-card h3 { margin-top: 0; } +.econ-list { list-style: none; padding: 0; margin: 0; } +.econ-list li { padding: 0.6rem 0; border-bottom: 1px solid var(--border); display: flex; gap: 0.7rem; font-size: 0.95rem; color: var(--muted); } +.econ-list li:last-child { border-bottom: none; } +.econ-list li b { color: #fff; } +.econ-list .k { color: var(--accent); flex-shrink: 0; } +.stat-tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.85rem; } +.stat-tile { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 1.1rem; text-align: center; } +.stat-tile .num { font-size: 1.7rem; font-weight: 800; color: #fff; } +.stat-tile .lbl { color: var(--muted); font-size: 0.82rem; margin-top: 0.25rem; } +.rails { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1rem; } +.rail { background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: 10px; padding: 0.5rem 0.85rem; font-size: 0.85rem; color: var(--muted); } +.rail b { color: #fff; } + +/* Steps */ +.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap: 1.1rem; } +.step { background: var(--panel); border: 1px solid var(--border); border-radius: 16px; padding: 1.5rem; position: relative; } +.step .n { position: absolute; top: -14px; left: 20px; width: 32px; height: 32px; border-radius: 50%; + background: linear-gradient(135deg, var(--accent), #ff7a00); color: #201200; font-weight: 800; display: flex; align-items: center; justify-content: center; } +.step h3 { margin: 0.5rem 0 0.5rem; font-size: 1.1rem; } +.step p { margin: 0; color: var(--muted); font-size: 0.92rem; } + +/* FAQ */ +.faq-list { max-width: 800px; margin: 0 auto; } +details.faq { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 0 1.25rem; margin-bottom: 0.75rem; } +details.faq summary { cursor: pointer; padding: 1.1rem 0; font-weight: 700; font-size: 1.02rem; list-style: none; display: flex; justify-content: space-between; align-items: center; } +details.faq summary::-webkit-details-marker { display: none; } +details.faq summary::after { content: '+'; color: var(--accent); font-size: 1.4rem; font-weight: 400; } +details.faq[open] summary::after { content: '\2212'; } +details.faq p { color: var(--muted); margin: 0 0 1.1rem; font-size: 0.95rem; } + +/* CTA band */ +.cta-band { text-align: center; background: linear-gradient(135deg, rgba(102,51,255,0.18), rgba(255,153,51,0.12)); border: 1px solid var(--border); border-radius: 24px; padding: 3rem 1.5rem; } +.cta-band h2 { font-size: clamp(1.6rem, 4vw, 2.4rem); margin: 0 0 0.75rem; } +.cta-band p { color: var(--muted); max-width: 560px; margin: 0 auto 1.75rem; } + +/* Footer */ +footer.site { border-top: 1px solid var(--border); padding: 2.5rem 0; color: var(--dim); font-size: 0.88rem; } +footer.site .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem; align-items: center; } +footer.site a { color: var(--muted); text-decoration: none; margin-left: 1.25rem; } +footer.site a:hover { color: #fff; } +.disclaimer { max-width: 1200px; margin: 1.5rem auto 0; padding: 0 1.25rem; color: var(--dim); font-size: 0.75rem; line-height: 1.5; } + +/* Responsive nav (replaces old display:none toggle) */ +.nav-menu { display: contents; } +.nav-toggle { display: none; cursor: pointer; font-size: 1.4rem; list-style: none; } +.nav-toggle::-webkit-details-marker { display: none; } +@media (max-width: 760px) { + .nav-menu { display: block; position: relative; } + .nav-toggle { display: block; } + .nav-menu .nav-links { + position: absolute; right: 0; top: 2.2rem; flex-direction: column; + background: rgba(4,5,20,0.97); border: 1px solid var(--border); + border-radius: 12px; padding: 0.75rem 1rem; gap: 0.75rem; min-width: 160px; z-index: 60; + } +} diff --git a/src/server.ts b/src/server.ts index 5ae3ec5..08c835e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,6 +25,7 @@ import leaderboardHandler from './backend/leaderboard-handler' import mintHandler from './backend/mint' import nftHandler from './backend/nft-handler' import productionHandler from './backend/production-handler' +import publicMetricsHandler from './backend/public-metrics' import pvpHandler from './backend/pvp-handler' import questHandler from './backend/quest-handler' import { createSeasonPassInvoiceHandler } from './backend/season-pass-invoice' @@ -135,6 +136,8 @@ export async function createServer(bot: Bot) { await server.register(leaderboardHandler, { prefix: '/api/users' }) await server.register(claimHandler, { prefix: '/api/users' }) + await server.register(publicMetricsHandler, { prefix: '/api/public' }) + await server.register(worldsHandler, { prefix: '/api/game' }) await server.register(productionHandler, { prefix: '/api/game' }) await server.register(castleUpgradeHandler, { prefix: '/api/game' }) @@ -160,10 +163,33 @@ export async function createServer(bot: Bot) { // is served under /game (Vite base '/game/'). Telegram must open /game. The // landing dir also holds tonconnect-manifest.json + logo.png, which TON Connect // and the manifest reference at the root origin. - const landingPath = path.join(__dirname, 'landing') + const landingPath = path.join(__dirname, 'landing', 'dist') const isGameUrl = (url: string) => url === '/game' || url.startsWith('/game/') + const resolveLandingFile = async (url: string): Promise => { + const clean = (url.split('?')[0] || '/').replace(/\/+$/, '') + const rel = clean === '' ? 'index.html' : clean.slice(1) + const candidates = rel.endsWith('.html') + ? [rel] + : [path.join(rel, 'index.html'), `${rel}.html`] + for (const candidate of candidates) { + const abs = path.join(landingPath, candidate) + // Guard against path traversal outside the landing dir. + if (!abs.startsWith(landingPath + path.sep)) { + continue + } + try { + await fs.access(abs) + return abs + } + catch { + // try next candidate + } + } + return null + } + if (config.NODE_ENV === 'development') { // Load vite from the frontend's own node_modules: the frontend's // vite.config.ts and plugins resolve there, and two copies of vite's @@ -198,12 +224,38 @@ export async function createServer(bot: Bot) { const html = await vite.transformIndexHtml(url, indexHtml) return reply.type('text/html').send(html) } - // Root and everything else → the static landing page. - const landingHtml = await fs.readFile( - path.join(landingPath, 'index.html'), - 'utf-8', - ) - return reply.type('text/html').send(landingHtml) + // Root and everything else → the multi-page landing dist. + // Serve non-HTML landing assets (CSS, images, etc.) in dev mode. + const clean = (url.split('?')[0] || '/').replace(/\/+$/, '') + const rel = clean === '' ? '' : clean.slice(1) + if (rel && !rel.endsWith('.html')) { + const staticAbs = path.join(landingPath, rel) + if (staticAbs.startsWith(landingPath + path.sep)) { + try { + const buf = await fs.readFile(staticAbs) + const ext = path.extname(staticAbs).toLowerCase() + const ct = ext === '.css' ? 'text/css' + : ext === '.js' ? 'application/javascript' + : ext === '.png' ? 'image/png' + : ext === '.svg' ? 'image/svg+xml' + : ext === '.json' ? 'application/json' + : ext === '.xml' ? 'application/xml' + : ext === '.txt' ? 'text/plain' + : 'application/octet-stream' + return reply.type(ct).send(buf) + } + catch { + // not found, fall through to HTML resolver + } + } + } + const file = await resolveLandingFile(url) + if (file) { + const html = await fs.readFile(file, 'utf-8') + return reply.type('text/html').send(html) + } + const notFound = await fs.readFile(path.join(landingPath, '404.html'), 'utf-8') + return reply.status(404).type('text/html').send(notFound) }) } else { // Landing (+ root assets: manifest, logo) at the root. @@ -217,7 +269,7 @@ export async function createServer(bot: Bot) { prefix: '/game/', decorateReply: false, }) - server.setNotFoundHandler({ preHandler: [] }, (req, reply) => { + server.setNotFoundHandler({ preHandler: [] }, async (req, reply) => { const url = req.raw.url || '/' if (url.startsWith('/api/')) { return reply.status(404).send({ error: 'API route not found' }) @@ -227,7 +279,11 @@ export async function createServer(bot: Bot) { .type('text/html') .sendFile('index.html', path.join(frontendPath, 'dist')) } - return reply.type('text/html').sendFile('index.html', landingPath) + const file = await resolveLandingFile(url) + if (file) { + return reply.type('text/html').sendFile(path.relative(landingPath, file), landingPath) + } + return reply.status(404).type('text/html').sendFile('404.html', landingPath) }) }