164 lines
4.3 KiB
TypeScript
164 lines
4.3 KiB
TypeScript
// „🎨 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)
|
||
}
|