import type { TemplateResult } from "lit"; import { html, LitElement, render } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { COSMETICS } from "../core/CosmeticSchemas"; import { UserSettings } from "../core/game/UserSettings"; import { PatternDecoder } from "../core/PatternDecoder"; import "./components/Difficulties"; import "./components/Maps"; import { translateText } from "./Utils"; @customElement("territory-patterns-modal") export class TerritoryPatternsModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; }; public previewButton: HTMLElement | null = null; public buttonWidth: number = 100; @state() private selectedPattern: string | undefined; @state() private lockedPatterns: string[] = []; @state() private lockedReasons: Record = {}; @state() private hoveredPattern: string | null = null; @state() private hoverPosition = { x: 0, y: 0 }; @state() private keySequence: string[] = []; @state() private showChocoPattern = false; public resizeObserver: ResizeObserver; private userSettings: UserSettings = new UserSettings(); constructor() { super(); this.checkPatternPermission(undefined, undefined); } connectedCallback() { super.connectedCallback(); this.selectedPattern = this.userSettings.getSelectedPattern(); window.addEventListener("keydown", this.handleKeyDown); this.updateComplete.then(() => { const containers = this.renderRoot.querySelectorAll(".preview-container"); if (this.resizeObserver) { containers.forEach((container) => this.resizeObserver.observe(container), ); } this.updatePreview(); }); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener("keydown", this.handleKeyDown); this.resizeObserver.disconnect(); } onLogout() { this.checkPatternPermission(undefined, undefined); } onUserMe(userMeResponse: UserMeResponse) { const { player } = userMeResponse; const { roles, flares } = player; this.checkPatternPermission(roles, flares); } private checkPatternPermission( roles: string[] | undefined, flares: string[] | undefined, ) { this.lockedPatterns = []; this.lockedReasons = {}; for (const key in COSMETICS.patterns) { const patternData = COSMETICS.patterns[key]; const roleGroup: string[] | string | undefined = patternData.role_group; if ( flares !== undefined && (flares.includes("pattern:*") || flares.includes(`pattern:${key}`)) ) { continue; } if (!roleGroup || (Array.isArray(roleGroup) && roleGroup.length === 0)) { if (roles === undefined || roles.length === 0) { const reason = translateText("territory_patterns.blocked.login"); this.setLockedPatterns([key], reason); } continue; } const groupList = Array.isArray(roleGroup) ? roleGroup : [roleGroup]; const isAllowed = roles !== undefined && groupList.some((required) => roles.includes(required)); if (!isAllowed) { const reason = translateText("territory_patterns.blocked.role", { role: groupList.join(", "), }); this.setLockedPatterns([key], reason); } } this.requestUpdate(); } private handleKeyDown = (e: KeyboardEvent) => { const key = e.key.toLowerCase(); const nextSequence = [...this.keySequence, key].slice(-5); this.keySequence = nextSequence; if (nextSequence.join("") === "choco") { this.triggerChocoEasterEgg(); this.keySequence = []; } }; private triggerChocoEasterEgg() { console.log("🍫 Choco pattern unlocked!"); this.showChocoPattern = true; const popup = document.createElement("div"); popup.className = "easter-egg-popup"; popup.textContent = "🎉 You unlocked the Choco pattern!"; document.body.appendChild(popup); setTimeout(() => { popup.remove(); }, 5000); this.requestUpdate(); } createRenderRoot() { return this; } private renderTooltip(): TemplateResult | null { if (this.hoveredPattern && this.lockedReasons[this.hoveredPattern]) { return html`
${this.lockedReasons[this.hoveredPattern]}
`; } return null; } private renderPatternButton(key: string): TemplateResult { const isLocked = this.isPatternLocked(key); const isSelected = this.selectedPattern === key; const name = COSMETICS.patterns[key]?.name ?? "custom"; return html` `; } private renderPatternGrid(): TemplateResult { const buttons: TemplateResult[] = []; for (const key in COSMETICS.patterns) { const value = COSMETICS.patterns[key]; if (!this.showChocoPattern && value.name === "choco") continue; const result = this.renderPatternButton(key); buttons.push(result); } return html`
${buttons}
`; } render() { return html` ${this.renderTooltip()} ${this.renderPatternGrid()} `; } public open() { this.modalEl?.open(); } public close() { this.modalEl?.close(); } private selectPattern(pattern: string | undefined) { this.userSettings.setSelectedPattern(pattern); this.selectedPattern = pattern; this.updatePreview(); this.close(); } private renderPatternPreview( pattern: string, width: number, height: number, ): TemplateResult { const decoder = new PatternDecoder(pattern); const cellCountX = decoder.getTileWidth(); const cellCountY = decoder.getTileHeight(); const cellSize = cellCountX > 0 && cellCountY > 0 ? Math.min(height / cellCountY, width / cellCountX) : 1; return html`
${(() => { const tiles: TemplateResult[] = []; for (let py = 0; py < cellCountY; py++) { for (let px = 0; px < cellCountX; px++) { const x = px << decoder.getScale(); const y = py << decoder.getScale(); const bit = decoder.isSet(x, y); tiles.push(html`
`); } } return tiles; })()}
`; } private renderBlankPreview(width: number, height: number): TemplateResult { return html`
`; } public updatePreview() { if (this.previewButton === null) return; const preview = this.selectedPattern === undefined ? this.renderBlankPreview(48, 48) : this.renderPatternPreview(this.selectedPattern, 48, 48); render(preview, this.previewButton); } private setLockedPatterns(lockedPatterns: string[], reason: string) { this.lockedPatterns = [...this.lockedPatterns, ...lockedPatterns]; this.lockedReasons = { ...this.lockedReasons, ...lockedPatterns.reduce( (acc, key) => { acc[key] = reason; return acc; }, {} as Record, ), }; } private isPatternLocked(patternKey: string): boolean { return this.lockedPatterns.includes(patternKey); } private handleMouseEnter(patternKey: string, event: MouseEvent) { if (this.isPatternLocked(patternKey)) { this.hoveredPattern = patternKey; this.hoverPosition = { x: event.clientX, y: event.clientY }; } } private handleMouseMove(event: MouseEvent) { if (this.hoveredPattern) { this.hoverPosition = { x: event.clientX, y: event.clientY }; } } private handleMouseLeave() { this.hoveredPattern = null; } }