Files
OpenFrontIO/src/core/PatternDecoder.ts
T
evanpelle a26585a47b 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
2025-09-18 20:00:15 -07:00

73 lines
1.9 KiB
TypeScript

import { PlayerPattern } from "./Schemas";
export class PatternDecoder {
private bytes: Uint8Array;
readonly height: number;
readonly width: number;
readonly scale: number;
constructor(
pattern: PlayerPattern,
base64urlDecode: (input: string) => Uint8Array,
) {
({
height: this.height,
width: this.width,
scale: this.scale,
bytes: this.bytes,
} = decodePatternData(pattern.patternData, base64urlDecode));
}
isPrimary(x: number, y: number): boolean {
const px = (x >> this.scale) % this.width;
const py = (y >> this.scale) % this.height;
const idx = py * this.width + px;
const byteIndex = idx >> 3;
const bitIndex = idx & 7;
const byte = this.bytes[3 + byteIndex];
if (byte === undefined) throw new Error("Invalid pattern");
return (byte & (1 << bitIndex)) === 0;
}
scaledHeight(): number {
return this.height << this.scale;
}
scaledWidth(): number {
return this.width << this.scale;
}
}
export function decodePatternData(
b64: string,
base64urlDecode: (input: string) => Uint8Array,
): { height: number; width: number; scale: number; bytes: Uint8Array } {
const bytes = base64urlDecode(b64);
if (bytes.length < 3) {
throw new Error("Pattern data is too short to contain required metadata.");
}
const version = bytes[0];
if (version !== 0) {
throw new Error(`Unrecognized pattern version ${version}.`);
}
const byte1 = bytes[1];
const byte2 = bytes[2];
const scale = byte1 & 0x07;
const width = (((byte2 & 0x03) << 5) | ((byte1 >> 3) & 0x1f)) + 2;
const height = ((byte2 >> 2) & 0x3f) + 2;
const expectedBits = width * height;
const expectedBytes = (expectedBits + 7) >> 3; // Equivalent to: ceil(expectedBits / 8);
if (bytes.length - 3 < expectedBytes) {
throw new Error("Pattern data is too short for the specified dimensions.");
}
return { height, width, scale, bytes };
}