Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
"choose_spawn": "Choose a starting location"
},
"territory_patterns": {
"title": "Select Territory Pattern",
"title": "Select Territory Skin",
"purchase": "Purchase",
"blocked": {
"login": "You must be logged in to access this pattern.",
Expand Down
3 changes: 2 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GameID,
GameRecord,
GameStartInfo,
PlayerPattern,
PlayerRecord,
ServerMessage,
} from "../core/Schemas";
Expand Down Expand Up @@ -47,7 +48,7 @@ import { createRenderer, GameRenderer } from "./graphics/GameRenderer";

export interface LobbyConfig {
serverConfig: ServerConfig;
patternName: string | undefined;
pattern: PlayerPattern | undefined;
flag: string;
playerName: string;
clientID: ClientID;
Expand Down
93 changes: 59 additions & 34 deletions src/client/Cosmetics.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
import { UserMeResponse } from "../core/ApiSchemas";
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
import {
ColorPalette,
Cosmetics,
CosmeticsSchema,
Pattern,
} from "../core/CosmeticSchemas";
import { getApiBase, getAuthHeader } from "./jwt";
import { getPersistentID } from "./Main";

export async function fetchPatterns(
userMe: UserMeResponse | null,
): Promise<Map<string, Pattern>> {
const cosmetics = await getCosmetics();

if (cosmetics === undefined) {
return new Map();
}

const patterns: Map<string, Pattern> = new Map();
const playerFlares = new Set(userMe?.player?.flares ?? []);
const hasAllPatterns = playerFlares.has("pattern:*");

for (const name in cosmetics.patterns) {
const patternData = cosmetics.patterns[name];
const hasAccess = hasAllPatterns || playerFlares.has(`pattern:${name}`);
if (hasAccess) {
// Remove product info because player already has access.
patternData.product = null;
patterns.set(name, patternData);
} else if (patternData.product !== null) {
// Player doesn't have access, but product is available for purchase.
patterns.set(name, patternData);
}
// If player doesn't have access and product is null, don't show it.
}
return patterns;
}

export async function handlePurchase(pattern: Pattern) {
export async function handlePurchase(
pattern: Pattern,
colorPalette: ColorPalette | null,
) {
if (pattern.product === null) {
alert("This pattern is not available for purchase.");
return;
Expand All @@ -50,6 +29,7 @@ export async function handlePurchase(pattern: Pattern) {
body: JSON.stringify({
priceId: pattern.product.priceId,
hostname: window.location.origin,
colorPaletteName: colorPalette?.name,
}),
},
);
Expand All @@ -72,20 +52,65 @@ export async function handlePurchase(pattern: Pattern) {
window.location.href = url;
}

export async function getCosmetics(): Promise<Cosmetics | undefined> {
export async function fetchCosmetics(): Promise<Cosmetics | null> {
try {
const response = await fetch(`${getApiBase()}/cosmetics.json`);
if (!response.ok) {
console.error(`HTTP error! status: ${response.status}`);
return;
return null;
}
const result = CosmeticsSchema.safeParse(await response.json());
if (!result.success) {
console.error(`Invalid cosmetics: ${result.error.message}`);
return;
return null;
}
return result.data;
} catch (error) {
console.error("Error getting cosmetics:", error);
return null;
}
}

export function patternRelationship(
pattern: Pattern,
colorPalette: { name: string; isArchived?: boolean } | null,
userMeResponse: UserMeResponse | null,
affiliateCode: string | null,
): "owned" | "purchasable" | "blocked" {
const flares = userMeResponse?.player.flares ?? [];
if (flares.includes("pattern:*")) {
return "owned";
}

if (colorPalette === null) {
// For backwards compatibility only show non-colored patterns if they are owned.
if (flares.includes(`pattern:${pattern.name}`)) {
return "owned";
}
return "blocked";
}

const requiredFlare = `pattern:${pattern.name}:${colorPalette.name}`;

if (flares.includes(requiredFlare)) {
return "owned";
}

if (pattern.product === null) {
// We don't own it and it's not for sale, so don't show it.
return "blocked";
}

if (colorPalette?.isArchived) {
// We don't own the color palette, and it's archived, so don't show it.
return "blocked";
}

if (affiliateCode !== pattern.affiliateCode) {
// Pattern is for sale, but it's not the right store to show it on.
return "blocked";
}

// Patterns is for sale, and it's the right store to show it on.
return "purchasable";
}
5 changes: 4 additions & 1 deletion src/client/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { UserSettings } from "../core/game/UserSettings";
import "./AccountModal";
import { joinLobby } from "./ClientGameRunner";
import { fetchCosmetics } from "./Cosmetics";
import "./DarkModeButton";
import { DarkModeButton } from "./DarkModeButton";
import "./FlagInput";
Expand Down Expand Up @@ -508,7 +509,9 @@ class Client {
{
gameID: lobby.gameID,
serverConfig: config,
patternName: this.userSettings.getSelectedPatternName(),
pattern:
this.userSettings.getSelectedPatternName(await fetchCosmetics()) ??
undefined,
flag:
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
? ""
Expand Down
27 changes: 14 additions & 13 deletions src/client/SinglePlayerModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import "./components/baseComponents/Modal";
import "./components/Difficulties";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import { getCosmetics } from "./Cosmetics";
import { fetchCosmetics } from "./Cosmetics";
import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main";
import { UsernameInput } from "./UsernameInput";
Expand Down Expand Up @@ -425,13 +425,12 @@ export class SinglePlayerModal extends LitElement {
if (!flagInput) {
console.warn("Flag input element not found");
}
const patternName = this.userSettings.getSelectedPatternName();
let pattern: string | undefined = undefined;
if (this.userSettings.getDevOnlyPattern()) {
pattern = this.userSettings.getDevOnlyPattern();
} else if (patternName) {
pattern = (await getCosmetics())?.patterns[patternName]?.pattern;
}
const cosmetics = await fetchCosmetics();
let selectedPattern = this.userSettings.getSelectedPatternName(cosmetics);
selectedPattern ??= cosmetics
? (this.userSettings.getDevOnlyPattern() ?? null)
: null;

this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
Expand All @@ -443,11 +442,13 @@ export class SinglePlayerModal extends LitElement {
{
clientID,
username: usernameInput.getCurrentUsername(),
flag:
flagInput.getCurrentFlag() === "xx"
? ""
: flagInput.getCurrentFlag(),
pattern: pattern,
cosmetics: {
flag:
flagInput.getCurrentFlag() === "xx"
? ""
: flagInput.getCurrentFlag(),
pattern: selectedPattern ?? undefined,
},
},
],
config: {
Expand Down
83 changes: 51 additions & 32 deletions src/client/TerritoryPatternsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ 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 { Pattern } from "../core/CosmeticSchemas";
import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas";
import { UserSettings } from "../core/game/UserSettings";
import { PlayerPattern } from "../core/Schemas";
import "./components/Difficulties";
import "./components/PatternButton";
import { renderPatternPreview } from "./components/PatternButton";
import { fetchPatterns, handlePurchase } from "./Cosmetics";
import {
fetchCosmetics,
handlePurchase,
patternRelationship,
} from "./Cosmetics";
import { translateText } from "./Utils";

@customElement("territory-patterns-modal")
Expand All @@ -19,16 +24,18 @@ export class TerritoryPatternsModal extends LitElement {

public previewButton: HTMLElement | null = null;

@state() private selectedPattern: Pattern | null;
@state() private selectedPattern: PlayerPattern | null;

private patterns: Map<string, Pattern> = new Map();
private cosmetics: Cosmetics | null = null;

private userSettings: UserSettings = new UserSettings();

private isActive = false;

private affiliateCode: string | null = null;

private userMeResponse: UserMeResponse | null = null;

constructor() {
super();
}
Expand All @@ -38,11 +45,12 @@ export class TerritoryPatternsModal extends LitElement {
this.userSettings.setSelectedPatternName(undefined);
this.selectedPattern = null;
}
this.patterns = await fetchPatterns(userMeResponse);
const storedPatternName = this.userSettings.getSelectedPatternName();
if (storedPatternName) {
this.selectedPattern = this.patterns.get(storedPatternName) ?? null;
}
this.userMeResponse = userMeResponse;
this.cosmetics = await fetchCosmetics();
this.selectedPattern =
this.cosmetics !== null
? this.userSettings.getSelectedPatternName(this.cosmetics)
: null;
this.refresh();
}

Expand All @@ -52,25 +60,31 @@ export class TerritoryPatternsModal extends LitElement {

private renderPatternGrid(): TemplateResult {
const buttons: TemplateResult[] = [];
for (const [name, pattern] of this.patterns) {
if (this.affiliateCode === null) {
if (pattern.affiliateCode !== null && pattern.product !== null) {
// Patterns with affiliate code are not for sale by default.
continue;
}
} else {
if (pattern.affiliateCode !== this.affiliateCode) {
for (const pattern of Object.values(this.cosmetics?.patterns ?? {})) {
const colorPalettes = [...(pattern.colorPalettes ?? []), null];
for (const colorPalette of colorPalettes) {
const rel = patternRelationship(
pattern,
colorPalette,
this.userMeResponse,
this.affiliateCode,
);
if (rel === "blocked") {
continue;
}
buttons.push(html`
<pattern-button
.pattern=${pattern}
.colorPalette=${this.cosmetics?.colorPalettes?.[
colorPalette?.name ?? ""
] ?? null}
.requiresPurchase=${rel === "purchasable"}
.onSelect=${(p: PlayerPattern | null) => this.selectPattern(p)}
.onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
handlePurchase(p, colorPalette)}
></pattern-button>
`);
}

buttons.push(html`
<pattern-button
.pattern=${pattern}
.onSelect=${(p: Pattern | null) => this.selectPattern(p)}
.onPurchase=${(p: Pattern) => handlePurchase(p)}
></pattern-button>
`);
}

return html`
Expand Down Expand Up @@ -115,19 +129,24 @@ export class TerritoryPatternsModal extends LitElement {
this.modalEl?.close();
}

private selectPattern(pattern: Pattern | null) {
this.userSettings.setSelectedPatternName(pattern?.name);
private selectPattern(pattern: PlayerPattern | null) {
if (pattern === null) {
this.userSettings.setSelectedPatternName(undefined);
} else {
const name =
pattern.colorPalette?.name === undefined
? pattern.name
: `${pattern.name}:${pattern.colorPalette.name}`;

this.userSettings.setSelectedPatternName(`pattern:${name}`);
}
this.selectedPattern = pattern;
this.refresh();
this.close();
}

public async refresh() {
const preview = renderPatternPreview(
this.selectedPattern?.pattern ?? null,
48,
48,
);
const preview = renderPatternPreview(this.selectedPattern ?? null, 48, 48);
this.requestUpdate();

// Wait for the DOM to be updated and the o-modal element to be available
Expand Down
7 changes: 5 additions & 2 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,11 @@ export class Transport {
lastTurn: numTurns,
token: this.lobbyConfig.token,
username: this.lobbyConfig.playerName,
flag: this.lobbyConfig.flag,
patternName: this.lobbyConfig.patternName,
cosmetics: {
flag: this.lobbyConfig.flag,
patternName: this.lobbyConfig.pattern?.name,
patternColorPaletteName: this.lobbyConfig.pattern?.colorPalette?.name,
},
} satisfies ClientJoinMessage);
}

Expand Down
Loading
Loading