Skip to content

Commit edc7ca5

Browse files
committed
color pattern
1 parent 25e8ec0 commit edc7ca5

28 files changed

+669
-499
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?.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: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ 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,13 +425,12 @@ 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;
434-
}
428+
const cosmetics = await fetchCosmetics();
429+
let selectedPattern = this.userSettings.getSelectedPatternName(cosmetics);
430+
selectedPattern ??= cosmetics
431+
? (this.userSettings.getDevOnlyPattern(cosmetics) ?? null)
432+
: null;
433+
435434
this.dispatchEvent(
436435
new CustomEvent("join-lobby", {
437436
detail: {
@@ -443,11 +442,13 @@ export class SinglePlayerModal extends LitElement {
443442
{
444443
clientID,
445444
username: usernameInput.getCurrentUsername(),
446-
flag:
447-
flagInput.getCurrentFlag() === "xx"
448-
? ""
449-
: flagInput.getCurrentFlag(),
450-
pattern: pattern,
445+
cosmetics: {
446+
flag:
447+
flagInput.getCurrentFlag() === "xx"
448+
? ""
449+
: flagInput.getCurrentFlag(),
450+
pattern: selectedPattern ?? undefined,
451+
},
451452
},
452453
],
453454
config: {

src/client/TerritoryPatternsModal.ts

Lines changed: 51 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,31 @@ 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+
const rel = patternRelationship(
67+
pattern,
68+
colorPalette,
69+
this.userMeResponse,
70+
this.affiliateCode,
71+
);
72+
if (rel === "blocked") {
6373
continue;
6474
}
75+
buttons.push(html`
76+
<pattern-button
77+
.pattern=${pattern}
78+
.colorPalette=${this.cosmetics?.colorPalettes?.[
79+
colorPalette?.name ?? ""
80+
] ?? null}
81+
.requiresPurchase=${rel === "purchasable"}
82+
.onSelect=${(p: PlayerPattern | null) => this.selectPattern(p)}
83+
.onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
84+
handlePurchase(p, colorPalette)}
85+
></pattern-button>
86+
`);
6587
}
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-
`);
7488
}
7589

7690
return html`
@@ -115,19 +129,24 @@ export class TerritoryPatternsModal extends LitElement {
115129
this.modalEl?.close();
116130
}
117131

118-
private selectPattern(pattern: Pattern | null) {
119-
this.userSettings.setSelectedPatternName(pattern?.name);
132+
private selectPattern(pattern: PlayerPattern | null) {
133+
if (pattern === null) {
134+
this.userSettings.setSelectedPatternName(undefined);
135+
} else {
136+
const name =
137+
pattern.colorPalette?.name === undefined
138+
? pattern.name
139+
: `${pattern.name}:${pattern.colorPalette.name}`;
140+
141+
this.userSettings.setSelectedPatternName(`pattern:${name}`);
142+
}
120143
this.selectedPattern = pattern;
121144
this.refresh();
122145
this.close();
123146
}
124147

125148
public async refresh() {
126-
const preview = renderPatternPreview(
127-
this.selectedPattern?.pattern ?? null,
128-
48,
129-
48,
130-
);
149+
const preview = renderPatternPreview(this.selectedPattern ?? null, 48, 48);
131150
this.requestUpdate();
132151

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

src/client/Transport.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,11 @@ export class Transport {
368368
lastTurn: numTurns,
369369
token: this.lobbyConfig.token,
370370
username: this.lobbyConfig.playerName,
371-
flag: this.lobbyConfig.flag,
372-
patternName: this.lobbyConfig.patternName,
371+
cosmetics: {
372+
flag: this.lobbyConfig.flag,
373+
patternName: this.lobbyConfig.pattern?.name,
374+
patternColorPaletteName: this.lobbyConfig.pattern?.colorPalette?.name,
375+
},
373376
} satisfies ClientJoinMessage);
374377
}
375378

0 commit comments

Comments
 (0)