Add support for colored patterns (#2062)

## Description:

Add support for colored territory patterns/skins

* Refactored & updated territory pattern rendering to render colored
skins
* rename public from pattern to skin (keep pattern name internally, too
difficult to rename)
* Moved all territory color logic to PlayerView
* Updated WinModal to show colored skins
* Refactored decode logic into a separate function: decodePatternData
* Refactored/updated how cosmetics are sent to server. Players now send
a PlayerCosmeticRefsSchema in the ClientJoinMessage.
PlayerCosmeticRefsSchema just contains names of the cosmetics, and the
server replaces the names/references with actual cosmetic data
* Refactored PastelThemeDark: have it extend Pastel theme so duplicate
logic can be removed.
* 

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
evanpelle
2025-09-18 20:00:15 -07:00
committed by GitHub
parent 25e8ec0579
commit a26585a47b
29 changed files with 650 additions and 495 deletions
+29 -12
View File
@@ -1,15 +1,17 @@
import { Cosmetics } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
import { decodePatternData } from "../core/PatternDecoder";
import { PlayerPattern } from "../core/Schemas";
type PatternResult =
| { type: "allowed"; pattern: string }
| { type: "allowed"; pattern: PlayerPattern }
| { type: "unknown" }
| { type: "forbidden"; reason: string };
export interface PrivilegeChecker {
isPatternAllowed(
base64: string,
flares: readonly string[] | undefined,
flares: readonly string[],
name: string,
colorPaletteName: string | null,
): PatternResult;
isCustomFlagAllowed(
flag: string,
@@ -24,25 +26,39 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
) {}
isPatternAllowed(
flares: readonly string[],
name: string,
flares: readonly string[] | undefined,
colorPaletteName: string | null,
): PatternResult {
// Look for the pattern in the cosmetics.json config
const found = this.cosmetics.patterns[name];
if (!found) return { type: "forbidden", reason: "pattern not found" };
try {
new PatternDecoder(found.pattern, this.b64urlDecode);
decodePatternData(found.pattern, this.b64urlDecode);
} catch (e) {
return { type: "forbidden", reason: "invalid pattern" };
}
if (
flares?.includes(`pattern:${found.name}`) ||
flares?.includes("pattern:*")
) {
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
if (flares.includes("pattern:*")) {
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
}
const flareName =
`pattern:${found.name}` +
(colorPaletteName ? `:${colorPaletteName}` : "");
if (flares.includes(flareName)) {
// Player has a flare for this pattern
return { type: "allowed", pattern: found.pattern };
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
} else {
return { type: "forbidden", reason: "no flares for pattern" };
}
@@ -124,8 +140,9 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
isPatternAllowed(
flares: readonly string[],
name: string,
flares: readonly string[] | undefined,
colorPaletteName: string | null,
): PatternResult {
return { type: "unknown" };
}