Skip to content

Commit adfc4a9

Browse files
committed
colored patterns
1 parent 25e8ec0 commit adfc4a9

27 files changed

+586
-425
lines changed

src/client/ClientGameRunner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
GameID,
66
GameRecord,
77
GameStartInfo,
8+
PlayerPattern,
89
PlayerRecord,
910
ServerMessage,
1011
} from "../core/Schemas";
@@ -47,7 +48,7 @@ import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
4748

4849
export interface LobbyConfig {
4950
serverConfig: ServerConfig;
50-
patternName: string | undefined;
51+
pattern: PlayerPattern | undefined;
5152
flag: string;
5253
playerName: string;
5354
clientID: ClientID;

src/client/Cosmetics.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,17 @@
11
import { UserMeResponse } from "../core/ApiSchemas";
2-
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
2+
import {
3+
ColorPalette,
4+
Cosmetics,
5+
CosmeticsSchema,
6+
Pattern,
7+
} from "../core/CosmeticSchemas";
38
import { getApiBase, getAuthHeader } from "./jwt";
49
import { getPersistentID } from "./Main";
510

6-
export async function fetchPatterns(
7-
userMe: UserMeResponse | null,
8-
): Promise<Map<string, Pattern>> {
9-
const cosmetics = await getCosmetics();
10-
11-
if (cosmetics === undefined) {
12-
return new Map();
13-
}
14-
15-
const patterns: Map<string, Pattern> = new Map();
16-
const playerFlares = new Set(userMe?.player?.flares ?? []);
17-
const hasAllPatterns = playerFlares.has("pattern:*");
18-
19-
for (const name in cosmetics.patterns) {
20-
const patternData = cosmetics.patterns[name];
21-
const hasAccess = hasAllPatterns || playerFlares.has(`pattern:${name}`);
22-
if (hasAccess) {
23-
// Remove product info because player already has access.
24-
patternData.product = null;
25-
patterns.set(name, patternData);
26-
} else if (patternData.product !== null) {
27-
// Player doesn't have access, but product is available for purchase.
28-
patterns.set(name, patternData);
29-
}
30-
// If player doesn't have access and product is null, don't show it.
31-
}
32-
return patterns;
33-
}
34-
35-
export async function handlePurchase(pattern: Pattern) {
11+
export async function handlePurchase(
12+
pattern: Pattern,
13+
colorPalette: ColorPalette | null,
14+
) {
3615
if (pattern.product === null) {
3716
alert("This pattern is not available for purchase.");
3817
return;
@@ -50,6 +29,7 @@ export async function handlePurchase(pattern: Pattern) {
5029
body: JSON.stringify({
5130
priceId: pattern.product.priceId,
5231
hostname: window.location.origin,
32+
colorPaletteName: colorPalette?.name,
5333
}),
5434
},
5535
);
@@ -72,20 +52,65 @@ export async function handlePurchase(pattern: Pattern) {
7252
window.location.href = url;
7353
}
7454

75-
export async function getCosmetics(): Promise<Cosmetics | undefined> {
55+
export async function fetchCosmetics(): Promise<Cosmetics | null> {
7656
try {
7757
const response = await fetch(`${getApiBase()}/cosmetics.json`);
7858
if (!response.ok) {
7959
console.error(`HTTP error! status: ${response.status}`);
80-
return;
60+
return null;
8161
}
8262
const result = CosmeticsSchema.safeParse(await response.json());
8363
if (!result.success) {
8464
console.error(`Invalid cosmetics: ${result.error.message}`);
85-
return;
65+
return null;
8666
}
8767
return result.data;
8868
} catch (error) {
8969
console.error("Error getting cosmetics:", error);
70+
return null;
71+
}
72+
}
73+
74+
export function patternRelationship(
75+
pattern: Pattern,
76+
colorPalette: { name: string; isArchived?: boolean } | null,
77+
userMeResponse: UserMeResponse | null,
78+
affiliateCode: string | null,
79+
): "owned" | "purchasable" | "blocked" {
80+
const flares = userMeResponse?.player.flares ?? [];
81+
if (flares.includes("pattern:*")) {
82+
return "owned";
9083
}
84+
85+
if (colorPalette === null) {
86+
// For backwards compatibility only show non-colored patterns if they are owned.
87+
if (flares.includes(`pattern:${pattern.name}`)) {
88+
return "owned";
89+
}
90+
return "blocked";
91+
}
92+
93+
const requiredFlare = `pattern:${pattern.name}:${colorPalette.name}`;
94+
95+
if (flares.includes(requiredFlare)) {
96+
return "owned";
97+
}
98+
99+
if (pattern.product === null) {
100+
// We don't own it and it's not for sale, so don't show it.
101+
return "blocked";
102+
}
103+
104+
if (colorPalette !== null && colorPalette.isArchived) {
105+
// We don't own the color palette, and it's archived, so don't show it.
106+
return "blocked";
107+
}
108+
109+
if (affiliateCode !== pattern.affiliateCode) {
110+
// Pattern is for sale, but it's not the right store to show it on.
111+
return "blocked";
112+
}
113+
114+
// Patterns is for sale, and it's the right store to show it on.
115+
return "purchasable";
91116
}

src/client/Main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
77
import { UserSettings } from "../core/game/UserSettings";
88
import "./AccountModal";
99
import { joinLobby } from "./ClientGameRunner";
10+
import { fetchCosmetics } from "./Cosmetics";
1011
import "./DarkModeButton";
1112
import { DarkModeButton } from "./DarkModeButton";
1213
import "./FlagInput";
@@ -508,7 +509,9 @@ class Client {
508509
{
509510
gameID: lobby.gameID,
510511
serverConfig: config,
511-
patternName: this.userSettings.getSelectedPatternName(),
512+
pattern:
513+
this.userSettings.getSelectedPatternName(await fetchCosmetics()) ??
514+
undefined,
512515
flag:
513516
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
514517
? ""

src/client/SinglePlayerModal.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import {
1414
mapCategories,
1515
} from "../core/game/Game";
1616
import { UserSettings } from "../core/game/UserSettings";
17-
import { TeamCountConfig } from "../core/Schemas";
17+
import { PlayerPattern, TeamCountConfig } from "../core/Schemas";
1818
import { generateID } from "../core/Util";
1919
import "./components/baseComponents/Button";
2020
import "./components/baseComponents/Modal";
2121
import "./components/Difficulties";
2222
import { DifficultyDescription } from "./components/Difficulties";
2323
import "./components/Maps";
24-
import { getCosmetics } from "./Cosmetics";
24+
import { fetchCosmetics } from "./Cosmetics";
2525
import { FlagInput } from "./FlagInput";
2626
import { JoinLobbyEvent } from "./Main";
2727
import { UsernameInput } from "./UsernameInput";
@@ -425,12 +425,20 @@ export class SinglePlayerModal extends LitElement {
425425
if (!flagInput) {
426426
console.warn("Flag input element not found");
427427
}
428-
const patternName = this.userSettings.getSelectedPatternName();
429-
let pattern: string | undefined = undefined;
430-
if (this.userSettings.getDevOnlyPattern()) {
431-
pattern = this.userSettings.getDevOnlyPattern();
432-
} else if (patternName) {
433-
pattern = (await getCosmetics())?.patterns[patternName]?.pattern;
428+
let selectedPattern = this.userSettings.getSelectedPatternName(
429+
await fetchCosmetics(),
430+
);
431+
if (selectedPattern === null && this.userSettings.getDevOnlyPattern()) {
432+
const devOnlyPattern = this.userSettings.getDevOnlyPattern();
433+
selectedPattern = {
434+
name: devOnlyPattern!,
435+
patternData: devOnlyPattern!,
436+
colorPalette: {
437+
name: "dev_pattern",
438+
primaryColor: "#000000",
439+
secondaryColor: "#ffffff",
440+
},
441+
} satisfies PlayerPattern;
434442
}
435443
this.dispatchEvent(
436444
new CustomEvent("join-lobby", {
@@ -443,11 +451,13 @@ export class SinglePlayerModal extends LitElement {
443451
{
444452
clientID,
445453
username: usernameInput.getCurrentUsername(),
446-
flag:
447-
flagInput.getCurrentFlag() === "xx"
448-
? ""
449-
: flagInput.getCurrentFlag(),
450-
pattern: pattern,
454+
cosmetics: {
455+
flag:
456+
flagInput.getCurrentFlag() === "xx"
457+
? ""
458+
: flagInput.getCurrentFlag(),
459+
pattern: selectedPattern ?? undefined,
460+
},
451461
},
452462
],
453463
config: {

src/client/TerritoryPatternsModal.ts

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import type { TemplateResult } from "lit";
22
import { html, LitElement, render } from "lit";
33
import { customElement, query, state } from "lit/decorators.js";
44
import { UserMeResponse } from "../core/ApiSchemas";
5-
import { Pattern } from "../core/CosmeticSchemas";
5+
import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas";
66
import { UserSettings } from "../core/game/UserSettings";
7+
import { PlayerPattern } from "../core/Schemas";
78
import "./components/Difficulties";
89
import "./components/PatternButton";
910
import { renderPatternPreview } from "./components/PatternButton";
10-
import { fetchPatterns, handlePurchase } from "./Cosmetics";
11+
import {
12+
fetchCosmetics,
13+
handlePurchase,
14+
patternRelationship,
15+
} from "./Cosmetics";
1116
import { translateText } from "./Utils";
1217

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

2025
public previewButton: HTMLElement | null = null;
2126

22-
@state() private selectedPattern: Pattern | null;
27+
@state() private selectedPattern: PlayerPattern | null;
2328

24-
private patterns: Map<string, Pattern> = new Map();
29+
private cosmetics: Cosmetics | null = null;
2530

2631
private userSettings: UserSettings = new UserSettings();
2732

2833
private isActive = false;
2934

3035
private affiliateCode: string | null = null;
3136

37+
private userMeResponse: UserMeResponse | null = null;
38+
3239
constructor() {
3340
super();
3441
}
@@ -38,11 +45,12 @@ export class TerritoryPatternsModal extends LitElement {
3845
this.userSettings.setSelectedPatternName(undefined);
3946
this.selectedPattern = null;
4047
}
41-
this.patterns = await fetchPatterns(userMeResponse);
42-
const storedPatternName = this.userSettings.getSelectedPatternName();
43-
if (storedPatternName) {
44-
this.selectedPattern = this.patterns.get(storedPatternName) ?? null;
45-
}
48+
this.userMeResponse = userMeResponse;
49+
this.cosmetics = await fetchCosmetics();
50+
this.selectedPattern =
51+
this.cosmetics !== null
52+
? this.userSettings.getSelectedPatternName(this.cosmetics)
53+
: null;
4654
this.refresh();
4755
}
4856

@@ -52,25 +60,37 @@ export class TerritoryPatternsModal extends LitElement {
5260

5361
private renderPatternGrid(): TemplateResult {
5462
const buttons: TemplateResult[] = [];
55-
for (const [name, pattern] of this.patterns) {
56-
if (this.affiliateCode === null) {
57-
if (pattern.affiliateCode !== null && pattern.product !== null) {
58-
// Patterns with affiliate code are not for sale by default.
59-
continue;
60-
}
61-
} else {
62-
if (pattern.affiliateCode !== this.affiliateCode) {
63+
for (const pattern of Object.values(this.cosmetics?.patterns ?? {})) {
64+
const colorPalettes = [...(pattern.colorPalettes ?? []), null];
65+
for (const colorPalette of colorPalettes) {
66+
if (
67+
patternRelationship(
68+
pattern,
69+
colorPalette,
70+
this.userMeResponse,
71+
this.affiliateCode,
72+
) === "blocked"
73+
) {
6374
continue;
6475
}
76+
buttons.push(html`
77+
<pattern-button
78+
.pattern=${pattern}
79+
.colorPalette=${this.cosmetics?.colorPalettes?.[
80+
colorPalette?.name ?? ""
81+
] ?? null}
82+
.requiresPurchase=${patternRelationship(
83+
pattern,
84+
colorPalette,
85+
this.userMeResponse,
86+
this.affiliateCode,
87+
) === "purchasable"}
88+
.onSelect=${(p: PlayerPattern | null) => this.selectPattern(p)}
89+
.onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
90+
handlePurchase(p, colorPalette)}
91+
></pattern-button>
92+
`);
6593
}
66-
67-
buttons.push(html`
68-
<pattern-button
69-
.pattern=${pattern}
70-
.onSelect=${(p: Pattern | null) => this.selectPattern(p)}
71-
.onPurchase=${(p: Pattern) => handlePurchase(p)}
72-
></pattern-button>
73-
`);
7494
}
7595

7696
return html`
@@ -90,6 +110,20 @@ export class TerritoryPatternsModal extends LitElement {
90110
</div>
91111
`;
92112
}
113+
patternRelationship(
114+
pattern: {
115+
name: string;
116+
pattern: string;
117+
affiliateCode: string | null;
118+
product: { productId: string; priceId: string; price: string } | null;
119+
colorPalettes?:
120+
| { name: string; isArchived?: boolean | undefined }[]
121+
| undefined;
122+
},
123+
colorPalette: { name: string; isArchived?: boolean | undefined } | null,
124+
) {
125+
throw new Error("Method not implemented.");
126+
}
93127

94128
render() {
95129
if (!this.isActive) return html``;
@@ -115,19 +149,24 @@ export class TerritoryPatternsModal extends LitElement {
115149
this.modalEl?.close();
116150
}
117151

118-
private selectPattern(pattern: Pattern | null) {
119-
this.userSettings.setSelectedPatternName(pattern?.name);
152+
private selectPattern(pattern: PlayerPattern | null) {
153+
if (pattern === null) {
154+
this.userSettings.setSelectedPatternName(undefined);
155+
return;
156+
}
157+
const name =
158+
pattern.colorPalette?.name === undefined
159+
? pattern.name
160+
: `${pattern.name}:${pattern.colorPalette.name}`;
161+
162+
this.userSettings.setSelectedPatternName(`pattern:${name}`);
120163
this.selectedPattern = pattern;
121164
this.refresh();
122165
this.close();
123166
}
124167

125168
public async refresh() {
126-
const preview = renderPatternPreview(
127-
this.selectedPattern?.pattern ?? null,
128-
48,
129-
48,
130-
);
169+
const preview = renderPatternPreview(this.selectedPattern ?? null, 48, 48);
131170
this.requestUpdate();
132171

133172
// Wait for the DOM to be updated and the o-modal element to be available

0 commit comments

Comments
 (0)