feat: Bilder und Texte für die Community-Posts in r/Ninelands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1cf5de9ae0
commit
099f884dcb
7 changed files with 637 additions and 1 deletions
9
PLAN.md
9
PLAN.md
|
|
@ -298,3 +298,12 @@ Abgabe: **9. Dezember 2026, 03:00 MEZ**. Eingereicht wird in „Best Daily Game
|
|||
- Credits-Fenster im Post geprüft: passt auch bei 320 px Breite (Knopf endet bei 413 von 512 px).
|
||||
- Pixelify-Ligatur „fi“ (sieht aus wie „A“) jetzt für die ganze App aus (`font-variant-ligatures: none`).
|
||||
- **Fallstrick:** `devvit.json` geändert → laufenden Playtest stoppen und neu starten.
|
||||
|
||||
### 2026-11-13 – Bilder für die Community-Posts
|
||||
|
||||
- `npm run capture:community` (`scripts/capture/community.{mjs,ts,html,css}`): zwei Bilder 1080 × 1350 (4:5, im Feed
|
||||
am Handy groß) – „How to play“ (vier Figuren mit beanspruchten Feldern, Gold je Gelände, Sterne) und „Choose your
|
||||
realm“ (Völker, Bretter, Banner mit Freischalt-Bedingung). Muster kommen aus `UNIT_DEFS` (Strahlen/Ring, Budget),
|
||||
Bedingungen aus `look.ts` – stimmen also nach Regeländerungen. Seite 540 × 675 mit Faktor 2 aufgenommen, Pixel scharf.
|
||||
- Das Skript meldet, wenn der Inhalt höher als das Bild ist („… px zu hoch“).
|
||||
- Texte der drei Posts: `docs/community-posts.md`.
|
||||
|
|
|
|||
69
docs/community-posts.md
Normal file
69
docs/community-posts.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Community-Posts in r/Ninelands (Entwürfe)
|
||||
|
||||
Drei eigene Posts zum Go-live, kurz nacheinander: How to play und Go-live anheften. Am selben Tag die App
|
||||
installieren – das legt sofort den Tagespost #1 an. Bilder erzeugen: `npm run capture:community` →
|
||||
`dist/marketing/community/` (1080 × 1350, Werte aus Engine und `src/shared/look.ts`). Video für Post 3:
|
||||
`npm run capture:trailer` → `dist/marketing/trailer.mp4`.
|
||||
|
||||
## 1 – How to play (Bildpost: `how-to-play.png`)
|
||||
|
||||
Titel: `How to play Ninelands`
|
||||
|
||||
```text
|
||||
Every day at 00:00 UTC a new 9×9 map appears as a post here. Everyone plays the same map.
|
||||
|
||||
**Goal:** earn gold and rule the nine lands.
|
||||
|
||||
- **Castle** (free, once): claims up to 5 cells in straight lines.
|
||||
- **Farmer** (free): claims the 8 cells around it, inside its own land. Must border your domain.
|
||||
- **Knight** (10 gold): 2 cells straight, stays in its land – pays double.
|
||||
- **Bishop** (15 gold): 2 cells diagonally, also across borders.
|
||||
|
||||
Claimed cells pay gold: ore and herbs 20, pasture 5 (7 next to a lake). Nothing can be built on claimed land, so
|
||||
plan where you build. You rule a land when you claim at least half of it.
|
||||
|
||||
**Stars:** ★ reach the gold goal, ★ rule the lands goal, ★ both in one game.
|
||||
**Law of the day:** each map has one special rule, e.g. "Costly knights".
|
||||
**End game:** when no move earns gold anymore, end the game to keep your gold.
|
||||
|
||||
Only your **first game** counts for the leaderboard – after that you can practice. Play daily for a 🔥 streak;
|
||||
yesterday's winner wears the 👑 flair.
|
||||
```
|
||||
|
||||
## 2 – Peoples & realms (Bildpost: `realms.png`)
|
||||
|
||||
Titel: `Choose your people: Kingdom, Orcs and Pirates`
|
||||
|
||||
```text
|
||||
Your realm, your look. Tap 🎨 Realm on the daily post:
|
||||
|
||||
- **Colours** – red, cyan, green, purple – free for everyone
|
||||
- **Peoples** – Kingdom, Orcs (20 ★), Pirates (40 ★)
|
||||
- **Boards** – Autumn (3-day streak), Winter (7), Night (14)
|
||||
- **Banners** – a new one every 10 stars
|
||||
- **Winterfest** (Dec 1 – Jan 6): snowy board and snowflake banner for everyone
|
||||
|
||||
Same rules for all – it's just about looking good while you win. Which people are you playing?
|
||||
```
|
||||
|
||||
## 3 – Go-live (Videopost: `trailer.mp4`, sonst `vimeo-cover.png`)
|
||||
|
||||
Titel: `Ninelands is live – a new 9×9 map every day`
|
||||
|
||||
```text
|
||||
Ninelands is a small strategy game I've been building – and now there's a daily version right here on Reddit.
|
||||
|
||||
Every day: one new map, one leaderboard, one first try. Play it in the daily post, compare your realm in the
|
||||
comments, keep your streak going.
|
||||
|
||||
Today's map is #1 – good luck, rulers!
|
||||
|
||||
New here? Read "How to play" (pinned). Bugs, ideas or feedback: comment below.
|
||||
|
||||
Original game: ninelands.stackteam.at
|
||||
Music: "Dreamscape (part I)" by Vee Ray (CC BY-SA 4.0) · Sprites: "Mini World Sprites" by Shade & octoshrimpy (CC0)
|
||||
```
|
||||
|
||||
Hinweise: Kosten in Post 1 sind die normalen (einzelne Gesetze ändern sie). „I've been building“ ist persönlich
|
||||
gemeint – als StackTeam „we“ schreiben. Ändern sich Kosten oder Freischaltungen, Bilder neu erzeugen und Texte
|
||||
anpassen.
|
||||
|
|
@ -18,7 +18,8 @@
|
|||
"publish": "npm run clean && npm run build && devvit publish",
|
||||
"watch": "sh -c 'trap \"kill 0\" exit; npm run build:client -- --watch=forever& npm run build:server -- --watch=forever& wait' --",
|
||||
"capture": "node scripts/capture/record.mjs",
|
||||
"capture:trailer": "node scripts/capture/trailer.mjs"
|
||||
"capture:trailer": "node scripts/capture/trailer.mjs",
|
||||
"capture:community": "node scripts/capture/community.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.6.6",
|
||||
|
|
|
|||
234
scripts/capture/community.css
Normal file
234
scripts/capture/community.css
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/* Bilder für die Community-Posts in r/Ninelands: 540 × 675 CSS-Pixel, aufgenommen mit Faktor 2 (1080 × 1350). */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 540px;
|
||||
height: 675px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sheet {
|
||||
box-sizing: border-box;
|
||||
width: 540px;
|
||||
height: 675px;
|
||||
padding: 14px 22px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-family: var(--font-pixel);
|
||||
color: var(--px-parchment);
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title h1 {
|
||||
margin: 0;
|
||||
font-size: 31px;
|
||||
color: var(--px-gold);
|
||||
text-shadow: 3px 3px 0 var(--px-ink);
|
||||
}
|
||||
|
||||
.title p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--px-parchment-dim);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--px-wood);
|
||||
border: 3px solid var(--px-ink);
|
||||
box-shadow: var(--px-shadow);
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
color: var(--px-gold);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.card small {
|
||||
font-size: 13px;
|
||||
color: var(--px-parchment);
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 2px 0 6px;
|
||||
font-family: var(--font-text);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
color: var(--px-parchment-dim);
|
||||
}
|
||||
|
||||
/* Regeltafel: 2 × 2 Figuren */
|
||||
.units {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pattern {
|
||||
display: grid;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
border: 2px solid var(--px-ink);
|
||||
}
|
||||
|
||||
.pattern .c {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.pattern .c.claim::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(150, 70, 255, 0.55);
|
||||
outline: 1px dashed rgba(255, 255, 255, 0.7);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.c > img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.gold-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.gold-item img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.gold-item b {
|
||||
color: var(--px-gold);
|
||||
}
|
||||
|
||||
.stars {
|
||||
color: var(--px-gold);
|
||||
}
|
||||
|
||||
.foot {
|
||||
margin: auto 0 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--px-parchment-dim);
|
||||
}
|
||||
|
||||
/* Realm-Übersicht */
|
||||
.peoples {
|
||||
display: grid;
|
||||
grid-template-columns: auto repeat(4, 1fr);
|
||||
align-items: center;
|
||||
gap: 2px 6px;
|
||||
}
|
||||
|
||||
.peoples img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.peoples .name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--px-gold);
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.patch {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
margin: 0 auto 3px;
|
||||
position: relative;
|
||||
border: 2px solid var(--px-ink);
|
||||
}
|
||||
|
||||
.patch span {
|
||||
background-size: cover;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.patch img {
|
||||
position: absolute;
|
||||
left: 22px;
|
||||
top: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.banners {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.banners svg {
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.stars-card {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.c > img.big {
|
||||
inset: -100%;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.gold-item small {
|
||||
font-size: 12px;
|
||||
color: var(--px-parchment-dim);
|
||||
}
|
||||
12
scripts/capture/community.html
Normal file
12
scripts/capture/community.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ninelands community images</title>
|
||||
<link rel="stylesheet" href="community.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="sheet"></main>
|
||||
<script src="community.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
91
scripts/capture/community.mjs
Normal file
91
scripts/capture/community.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env node
|
||||
// Bilder für die Community-Posts in r/Ninelands (1080 × 1350, hochkant für den Feed am Handy).
|
||||
// Aufruf: npm run capture:community → dist/marketing/community/how-to-play.png, realms.png
|
||||
import {copyFileSync, mkdirSync, readFileSync, statSync} from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
import {dirname, extname, join, resolve} from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
import {build} from 'esbuild'
|
||||
import {chromium} from 'playwright'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const site = join(root, 'dist/community')
|
||||
const out = join(root, 'dist/marketing/community')
|
||||
mkdirSync(site, {recursive: true})
|
||||
mkdirSync(out, {recursive: true})
|
||||
|
||||
await build({
|
||||
entryPoints: [join(root, 'scripts/capture/community.ts')],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
outdir: site,
|
||||
logLevel: 'warning',
|
||||
define: {
|
||||
'import.meta.env.MODE': '"reddit"',
|
||||
'import.meta.env.BASE_URL': '"/"',
|
||||
__API_ORIGIN__: '""',
|
||||
},
|
||||
loader: {'.woff2': 'file', '.woff': 'file'},
|
||||
assetNames: 'fonts/[name]-[hash]',
|
||||
})
|
||||
copyFileSync(
|
||||
join(root, 'scripts/capture/community.html'),
|
||||
join(site, 'index.html'),
|
||||
)
|
||||
|
||||
const types = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.png': 'image/png',
|
||||
'.woff2': 'font/woff2',
|
||||
'.woff': 'font/woff',
|
||||
}
|
||||
const server = createServer((req, res) => {
|
||||
const path = decodeURIComponent((req.url ?? '/').split('?')[0])
|
||||
for (const dir of [site, join(root, 'public')]) {
|
||||
const file = join(dir, path === '/' ? 'index.html' : path)
|
||||
try {
|
||||
if (file.startsWith(dir) && statSync(file).isFile()) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': types[extname(file)] ?? 'application/octet-stream',
|
||||
})
|
||||
res.end(readFileSync(file))
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
})
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
||||
const base = `http://127.0.0.1:${server.address().port}/`
|
||||
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
for (const page of ['howto', 'realms']) {
|
||||
const p = await browser.newPage({
|
||||
viewport: {width: 540, height: 675},
|
||||
deviceScaleFactor: 2,
|
||||
})
|
||||
p.on('pageerror', e => console.error(`Seitenfehler: ${e.message}`))
|
||||
await p.goto(`${base}?page=${page}`)
|
||||
await p.evaluate(() => document.fonts.ready)
|
||||
await p.waitForLoadState('networkidle')
|
||||
// Passt alles aufs Bild?
|
||||
const over = await p.evaluate(() => {
|
||||
const s = document.getElementById('sheet')
|
||||
return s ? s.scrollHeight - s.clientHeight : 0
|
||||
})
|
||||
if (over > 0) console.error(`${page}: ${over} px zu hoch`)
|
||||
const name = page === 'howto' ? 'how-to-play.png' : 'realms.png'
|
||||
await p.screenshot({path: join(out, name)})
|
||||
console.log(
|
||||
`${name} ${Math.round(statSync(join(out, name)).size / 1024)} KB`,
|
||||
)
|
||||
await p.close()
|
||||
}
|
||||
} finally {
|
||||
await browser.close()
|
||||
server.close()
|
||||
}
|
||||
220
scripts/capture/community.ts
Normal file
220
scripts/capture/community.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Bilder für die Community-Posts (How to play, Realms). Werte kommen aus Engine und look.ts, damit die Bilder
|
||||
// stimmen, wenn sich Regeln oder Freischaltungen ändern. ?page=howto | realms
|
||||
import '../../src/client/style.css'
|
||||
import './community.css'
|
||||
import {h} from '../../src/client/dom.ts'
|
||||
import {UNIT_NAME} from '../../src/client/texts.ts'
|
||||
import {spriteUrl} from '../../src/engine/app/board/atlas.ts'
|
||||
import {
|
||||
bannerPreview,
|
||||
pieceSpriteName,
|
||||
} from '../../src/engine/app/board/BoardView.ts'
|
||||
import {TEAM_HEX} from '../../src/engine/core/avatars.ts'
|
||||
import {
|
||||
UNIT_DEFS,
|
||||
UNIT_TYPES,
|
||||
type UnitType,
|
||||
} from '../../src/engine/core/units.ts'
|
||||
import {
|
||||
BANNER_OPTIONS,
|
||||
COLORS,
|
||||
FACTION_OPTIONS,
|
||||
THEME_OPTIONS,
|
||||
type Unlock,
|
||||
unlockText,
|
||||
} from '../../src/shared/look.ts'
|
||||
|
||||
const sheet = document.getElementById('sheet') as HTMLElement
|
||||
const COLOR = 'purple'
|
||||
|
||||
/** Normale Kosten (ohne Gesetz des Tages) */
|
||||
const COST: Record<UnitType, string> = {
|
||||
HIVEMOTHER: 'free',
|
||||
HIVEWORKER: 'free',
|
||||
DRONE: '10 gold',
|
||||
HIVESCOUT: '15 gold',
|
||||
}
|
||||
const RULE: Record<UnitType, string> = {
|
||||
HIVEMOTHER: 'Only once. Up to 5 cells per line.',
|
||||
HIVEWORKER: 'The 8 cells around it, own land.',
|
||||
DRONE: '2 cells per line, own land. Pays 2×.',
|
||||
HIVESCOUT: '2 cells per diagonal, any land.',
|
||||
}
|
||||
|
||||
/** Beanspruchte Felder auf freiem Grasland – direkt aus den Engine-Daten (Strahlen bzw. Ring) */
|
||||
function claimed(unit: UnitType, n: number): Set<number> {
|
||||
const def = UNIT_DEFS[unit]
|
||||
const mid = (n - 1) / 2
|
||||
const out = new Set<number>()
|
||||
for (const [dx, dy] of def.directions) {
|
||||
const steps = def.shape === 'ring' ? 1 : def.budget
|
||||
for (let s = 1; s <= steps; s++) {
|
||||
const x = mid + dx * s
|
||||
const y = mid + dy * s
|
||||
if (x >= 0 && y >= 0 && x < n && y < n) out.add(y * n + x)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pattern(unit: UnitType): HTMLElement {
|
||||
const n = unit === 'HIVEMOTHER' ? 11 : 5
|
||||
const size = unit === 'HIVEMOTHER' ? 12 : 26
|
||||
const grid = h('div.pattern')
|
||||
grid.style.gridTemplateColumns = `repeat(${n}, ${size}px)`
|
||||
grid.style.gridAutoRows = `${size}px`
|
||||
const claim = claimed(unit, n)
|
||||
for (let i = 0; i < n * n; i++) {
|
||||
const c = h('span.c')
|
||||
c.style.backgroundImage = `url(${spriteUrl(`grass${(i * 7) % 6}` as 'grass0')})`
|
||||
if (claim.has(i)) c.classList.add('claim')
|
||||
if (i === (n * n - 1) / 2) {
|
||||
const piece = img(pieceSpriteName(unit, COLOR, 'kingdom'))
|
||||
// Burg auf dem großen Raster dreifach groß, sonst kaum zu erkennen
|
||||
if (n > 5) piece.classList.add('big')
|
||||
c.append(piece)
|
||||
}
|
||||
grid.append(c)
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
function img(name: string): HTMLImageElement {
|
||||
const el = h('img')
|
||||
el.src = name.startsWith('/') ? name : spriteUrl(name as 'grass0')
|
||||
el.alt = ''
|
||||
return el
|
||||
}
|
||||
|
||||
function howto(): void {
|
||||
// Reihenfolge wie man spielt: Burg, Bauer, Ritter, Läufer
|
||||
const order: UnitType[] = ['HIVEMOTHER', 'HIVEWORKER', 'DRONE', 'HIVESCOUT']
|
||||
if (order.some(u => !UNIT_TYPES.includes(u))) throw new Error('Einheiten')
|
||||
sheet.append(
|
||||
h(
|
||||
'header.title',
|
||||
h('h1', 'How to play'),
|
||||
h('p', 'Ninelands · a new 9×9 map every day'),
|
||||
),
|
||||
h(
|
||||
'section.units',
|
||||
...order.map(u =>
|
||||
h(
|
||||
'div.card',
|
||||
h('h2', UNIT_NAME[u], h('small', COST[u])),
|
||||
h('p', RULE[u]),
|
||||
pattern(u),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
'section.card',
|
||||
h('h2', 'Claimed cells pay gold'),
|
||||
h(
|
||||
'div.row',
|
||||
h('span.gold-item', img('ore'), 'Ore ', h('b', '20')),
|
||||
h('span.gold-item', img('herbs'), 'Herbs ', h('b', '20')),
|
||||
h(
|
||||
'span.gold-item',
|
||||
img('grass0'),
|
||||
'Pasture ',
|
||||
h('b', '5'),
|
||||
h('small', ' (7 by a lake)'),
|
||||
),
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
'Claimed land can’t be built on – plan ahead. Claim half of a land to rule it.',
|
||||
),
|
||||
),
|
||||
h(
|
||||
'section.card.stars-card',
|
||||
h('span.stars', '★'),
|
||||
' gold goal ',
|
||||
h('span.stars', '★'),
|
||||
' lands goal ',
|
||||
h('span.stars', '★'),
|
||||
' both in one game',
|
||||
),
|
||||
h(
|
||||
'p.foot',
|
||||
'Only your first game counts · each map has its own law · new map 00:00 UTC',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const NAMES: Record<string, string> = {
|
||||
kingdom: 'Kingdom',
|
||||
orc: 'Orcs',
|
||||
pirate: 'Pirates',
|
||||
classic: 'Classic',
|
||||
autumn: 'Autumn',
|
||||
winter: 'Winter',
|
||||
night: 'Night',
|
||||
winterfest: 'Winterfest',
|
||||
}
|
||||
|
||||
const lock = (u: Unlock) =>
|
||||
u.kind === 'free' ? h('span.lock', 'free') : h('span.lock', unlockText(u))
|
||||
|
||||
function realms(): void {
|
||||
const peoples = h('div.peoples')
|
||||
FACTION_OPTIONS.forEach((f, row) => {
|
||||
peoples.append(h('span.name', NAMES[f.id] ?? f.id, lock(f.unlock)))
|
||||
for (const u of UNIT_TYPES) {
|
||||
// Jede Zeile in einer anderen Farbe – zeigt Völker und Farben zugleich
|
||||
const color = COLORS[(row + 1) % COLORS.length]?.id ?? 'red'
|
||||
peoples.append(img(pieceSpriteName(u, color, f.id)))
|
||||
}
|
||||
})
|
||||
const tiles = h(
|
||||
'div.tiles',
|
||||
...THEME_OPTIONS.map(t => {
|
||||
const patch = h('div.patch')
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const s = h('span')
|
||||
const file = `grass${(i * 5) % 6}.png`
|
||||
s.style.backgroundImage = `url(/sprites/themes/${t.id}/${file})`
|
||||
patch.append(s)
|
||||
}
|
||||
patch.append(img(pieceSpriteName('HIVEMOTHER', COLOR, 'kingdom')))
|
||||
return h('div.tile', patch, NAMES[t.id] ?? t.id, lock(t.unlock))
|
||||
}),
|
||||
)
|
||||
const banners = h(
|
||||
'div.banners',
|
||||
...BANNER_OPTIONS.map(b =>
|
||||
h(
|
||||
'div',
|
||||
bannerPreview(b.id, TEAM_HEX[COLOR]),
|
||||
`${b.id[0]?.toUpperCase()}${b.id.slice(1)}`,
|
||||
lock(b.unlock),
|
||||
),
|
||||
),
|
||||
)
|
||||
sheet.append(
|
||||
h(
|
||||
'header.title',
|
||||
h('h1', 'Choose your realm'),
|
||||
h(
|
||||
'p',
|
||||
'Tap 🎨 Realm on the daily post · 4 colours free · same rules for all',
|
||||
),
|
||||
),
|
||||
h('section.card', h('h2', 'Peoples', h('small', 'collect stars')), peoples),
|
||||
h('section.card', h('h2', 'Boards', h('small', 'keep your streak')), tiles),
|
||||
h(
|
||||
'section.card',
|
||||
h('h2', 'Banners', h('small', 'a new one every 10 ★')),
|
||||
banners,
|
||||
),
|
||||
h(
|
||||
'p.foot',
|
||||
'It’s all about looking good while you win – which people are you playing?',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const page = new URLSearchParams(location.search).get('page')
|
||||
if (page === 'realms') realms()
|
||||
else howto()
|
||||
Loading…
Reference in a new issue