ninelands-reddit/src/client/realm.ts
Lukas Müllner 51d47cd0d0 feat: Realm-Auswahl (Farbe, Volk, Thema, Banner), Freischaltungen, Flair, Ergebnis als Kommentar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 17:33:53 +01:00

164 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// „🎨 Realm“: Aussehen wählen (Farbe, Volk, Thema, Banner). Gesperrtes zeigt seine Bedingung – das lockt zum
// Wiederkommen. Gespeichert wird in Redis (localStorage leert Devvit bei jedem App-Update).
import {showLoginPrompt} from '@devvit/web/client'
import {spriteUrl} from '../engine/app/board/atlas.ts'
import {
bannerPreview,
pieceSpriteName,
setBoardBanners,
setBoardLook,
setBoardTheme,
} from '../engine/app/board/BoardView.ts'
import {TEAM_HEX} from '../engine/core/avatars.ts'
import {Endpoint, type SetLookRsp} from '../shared/api.ts'
import {
BANNER_OPTIONS,
COLORS,
FACTION_OPTIONS,
isUnlocked,
type Look,
type Progress,
THEME_OPTIONS,
type Unlock,
unlockText,
} from '../shared/look.ts'
import {h} from './dom.ts'
/** Aussehen auf alle Bretter anwenden (vor setBoard/update aufrufen) */
export function applyLook(look: Look): void {
setBoardTheme(look.theme)
setBoardBanners([look.banner, look.banner])
setBoardLook([look.color, look.color], [look.faction, look.faction])
}
const NAMES: Record<string, string> = {
red: 'Red',
cyan: 'Cyan',
lime: 'Green',
purple: 'Purple',
kingdom: 'Kingdom',
orc: 'Orcs',
pirate: 'Pirates',
classic: 'Classic',
autumn: 'Autumn',
winter: 'Winter',
night: 'Night',
winterfest: 'Winterfest',
pennant: 'Pennant',
swallowtail: 'Swallowtail',
hanging: 'Hanging',
stripe: 'Stripe',
cross: 'Cross',
star: 'Star',
crown: 'Crown',
snowflake: 'Snowflake',
}
function img(src: string): HTMLImageElement {
const el = h('img.realm-img')
el.src = src
el.alt = ''
return el
}
function themeTile(theme: string): HTMLImageElement {
return img(
theme === 'classic'
? spriteUrl('grass0')
: `/sprites/themes/${theme}/grass0.png`,
)
}
export function openRealm(opts: {
look: Look
progress: Progress
day: string
loggedIn: boolean
onChange: (look: Look) => void
}): void {
let look = opts.look
const body = h('div.realm-rows')
const close = h('button.primary', 'Done')
const overlay = h(
'div.overlay',
h(
'div.panel.realm',
h('h2', 'Your realm'),
h(
'p.muted.small',
opts.loggedIn
? 'Play daily to unlock more: themes come with your streak, banners and peoples with your stars.'
: 'Log in to customise your realm and keep your unlocks.',
),
body,
h('div.row.end', close),
),
)
close.addEventListener('click', () => overlay.remove())
const row = <T extends string>(
title: string,
options: readonly {id: T; unlock: Unlock}[],
key: keyof Look,
preview: (id: T) => Node,
) => {
const items = h('div.realm-items')
for (const o of options) {
const open = isUnlocked(o.unlock, opts.progress, opts.day)
const btn = h(
'button.realm-item',
preview(o.id),
h('span.realm-name', NAMES[o.id] ?? o.id),
open ? null : h('span.realm-lock', `🔒 ${unlockText(o.unlock)}`),
)
btn.classList.toggle('selected', look[key] === o.id)
btn.classList.toggle('locked', !open)
btn.disabled = !open
btn.addEventListener('click', () => void choose({...look, [key]: o.id}))
items.append(btn)
}
return h('section.realm-row', h('h3', title), items)
}
const render = () => {
body.replaceChildren(
row('Colour', COLORS, 'color', c =>
img(spriteUrl(pieceSpriteName('HIVEMOTHER', c, look.faction))),
),
row('People', FACTION_OPTIONS, 'faction', f =>
img(spriteUrl(pieceSpriteName('HIVEMOTHER', look.color, f))),
),
row('Board', THEME_OPTIONS, 'theme', t => themeTile(t)),
row('Banner', BANNER_OPTIONS, 'banner', b =>
bannerPreview(b, TEAM_HEX[look.color]),
),
)
}
async function choose(next: Look): Promise<void> {
if (!opts.loggedIn) {
showLoginPrompt()
return
}
look = next
render()
try {
const rsp = await fetch(Endpoint.SetLook, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({look: next}),
})
if (rsp.ok) look = ((await rsp.json()) as SetLookRsp).look
} catch {
// offline o. Ä.: Auswahl gilt bis zum Neuladen
}
render()
opts.onChange(look)
}
render()
document.body.append(overlay)
}