Merge branch 'v25'

This commit is contained in:
evanpelle
2025-09-26 10:53:57 -07:00
31 changed files with 679 additions and 496 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ import {
GameID,
GameRecord,
GameStartInfo,
PlayerPattern,
PlayerRecord,
ServerMessage,
} from "../core/Schemas";
@@ -49,7 +50,7 @@ import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
export interface LobbyConfig {
serverConfig: ServerConfig;
patternName: string | undefined;
pattern: PlayerPattern | undefined;
flag: string;
playerName: string;
clientID: ClientID;
+59 -34
View File
@@ -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;
@@ -50,6 +29,7 @@ export async function handlePurchase(pattern: Pattern) {
body: JSON.stringify({
priceId: pattern.product.priceId,
hostname: window.location.origin,
colorPaletteName: colorPalette?.name,
}),
},
);
@@ -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";
}
+1
View File
@@ -187,6 +187,7 @@ export class LocalServer {
username: this.lobbyConfig.playerName,
clientID: this.lobbyConfig.clientID,
stats: this.allPlayersStats[this.lobbyConfig.clientID],
cosmetics: this.lobbyConfig.gameStartInfo?.players[0].cosmetics,
},
];
if (this.lobbyConfig.gameStartInfo === undefined) {
+4 -1
View File
@@ -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";
@@ -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"
? ""
+18 -13
View File
@@ -17,7 +17,11 @@ import {
import { UserSettings } from "../core/game/UserSettings";
import { TeamCountConfig } from "../core/Schemas";
import { generateID } from "../core/Util";
import { getCosmetics } from "./Cosmetics";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import "./components/Maps";
import { fetchCosmetics } from "./Cosmetics";
import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main";
import { UsernameInput } from "./UsernameInput";
@@ -439,13 +443,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: {
@@ -457,11 +460,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: {
+51 -32
View File
@@ -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")
@@ -19,9 +24,9 @@ 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();
@@ -29,6 +34,8 @@ export class TerritoryPatternsModal extends LitElement {
private affiliateCode: string | null = null;
private userMeResponse: UserMeResponse | null = null;
constructor() {
super();
}
@@ -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();
}
@@ -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`
@@ -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
+5 -2
View File
@@ -377,8 +377,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);
}
+82 -32
View File
@@ -1,8 +1,14 @@
import { Colord } from "colord";
import { base64url } from "jose";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Pattern } from "../../core/CosmeticSchemas";
import {
ColorPalette,
DefaultPattern,
Pattern,
} from "../../core/CosmeticSchemas";
import { PatternDecoder } from "../../core/PatternDecoder";
import { PlayerPattern } from "../../core/Schemas";
import { translateText } from "../Utils";
export const BUTTON_WIDTH = 150;
@@ -12,17 +18,23 @@ export class PatternButton extends LitElement {
@property({ type: Object })
pattern: Pattern | null = null;
@property({ type: Function })
onSelect?: (pattern: Pattern | null) => void;
@property({ type: Object })
colorPalette: ColorPalette | null = null;
@property({ type: Boolean })
requiresPurchase: boolean = false;
@property({ type: Function })
onPurchase?: (pattern: Pattern) => void;
onSelect?: (pattern: PlayerPattern | null) => void;
@property({ type: Function })
onPurchase?: (pattern: Pattern, colorPalette: ColorPalette | null) => void;
createRenderRoot() {
return this;
}
private translatePatternName(prefix: string, patternName: string): string {
private translateCosmetic(prefix: string, patternName: string): string {
const translation = translateText(`${prefix}.${patternName}`);
if (translation.startsWith(prefix)) {
return patternName
@@ -35,55 +47,75 @@ export class PatternButton extends LitElement {
}
private handleClick() {
const isDefaultPattern = this.pattern === null;
if (isDefaultPattern || this.pattern?.product === null) {
this.onSelect?.(this.pattern);
if (this.pattern === null) {
this.onSelect?.(null);
return;
}
this.onSelect?.({
name: this.pattern!.name,
patternData: this.pattern!.pattern,
colorPalette: this.colorPalette ?? undefined,
} satisfies PlayerPattern);
}
private handlePurchase(e: Event) {
e.stopPropagation();
if (this.pattern?.product) {
this.onPurchase?.(this.pattern);
this.onPurchase?.(this.pattern, this.colorPalette ?? null);
}
}
render() {
const isDefaultPattern = this.pattern === null;
const isPurchasable = !isDefaultPattern && this.pattern?.product !== null;
return html`
<div
class="flex flex-col items-center gap-2 p-3 bg-white/10 rounded-lg max-w-[200px]"
class="flex flex-col items-center gap-1 p-1 bg-white/10 rounded-lg max-w-[200px]"
>
<button
class="bg-white/90 border-2 border-black/10 rounded-lg p-2 cursor-pointer transition-all duration-200 w-full
class="bg-white/90 border-2 border-black/10 rounded-lg cursor-pointer transition-all duration-200 w-full
hover:bg-white hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/20
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none"
?disabled=${isPurchasable}
?disabled=${this.requiresPurchase}
@click=${this.handleClick}
>
<div class="text-sm font-bold text-gray-800 mb-2 text-center">
<div class="text-sm font-bold text-gray-800 mb-1 text-center">
${isDefaultPattern
? translateText("territory_patterns.pattern.default")
: this.translatePatternName(
: this.translateCosmetic(
"territory_patterns.pattern",
this.pattern!.name,
)}
</div>
${this.colorPalette !== null
? html`
<div class="text-xs font-bold text-gray-800 mb-1 text-center">
${this.translateCosmetic(
"territory_patterns.color_palette",
this.colorPalette!.name,
)}
</div>
`
: null}
<div
class="w-[120px] h-[120px] flex items-center justify-center bg-white rounded p-1 mx-auto"
style="overflow: hidden;"
>
${renderPatternPreview(
this.pattern?.pattern ?? null,
this.pattern !== null
? ({
name: this.pattern!.name,
patternData: this.pattern!.pattern,
colorPalette: this.colorPalette ?? undefined,
} satisfies PlayerPattern)
: DefaultPattern,
BUTTON_WIDTH,
BUTTON_WIDTH,
)}
</div>
</button>
${isPurchasable
${this.requiresPurchase
? html`
<button
class="w-full px-4 py-2 bg-green-500 text-white border-0 rounded-md text-sm font-semibold cursor-pointer transition-colors duration-200
@@ -101,16 +133,16 @@ export class PatternButton extends LitElement {
}
export function renderPatternPreview(
pattern: string | null,
pattern: PlayerPattern | null,
width: number,
height: number,
): TemplateResult {
console.log("renderPatternPreview", pattern);
if (pattern === null) {
return renderBlankPreview(width, height);
}
const dataUrl = generatePreviewDataUrl(pattern, width, height);
return html`<img
src="${dataUrl}"
src="${generatePreviewDataUrl(pattern, width, height)}"
alt="Pattern preview"
class="w-full h-full object-contain"
style="image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;"
@@ -155,16 +187,21 @@ function renderBlankPreview(width: number, height: number): TemplateResult {
}
const patternCache = new Map<string, string>();
const DEFAULT_PATTERN_B64 = "AAAAAA"; // Empty 2x2 pattern
const COLOR_SET = [0, 0, 0, 255]; // Black
const COLOR_UNSET = [255, 255, 255, 255]; // White
const DEFAULT_PRIMARY = new Colord("#ffffff").toRgb(); // White
const DEFAULT_SECONDARY = new Colord("#000000").toRgb(); // Black
function generatePreviewDataUrl(
pattern?: string,
pattern?: PlayerPattern,
width?: number,
height?: number,
): string {
pattern ??= DEFAULT_PATTERN_B64;
const patternLookupKey = `${pattern}-${width}-${height}`;
pattern ??= DefaultPattern;
const patternLookupKey = [
pattern.name,
pattern.colorPalette?.primaryColor ?? "undefined",
pattern.colorPalette?.secondaryColor ?? "undefined",
width,
height,
].join("-");
if (patternCache.has(patternLookupKey)) {
return patternCache.get(patternLookupKey)!;
@@ -173,7 +210,14 @@ function generatePreviewDataUrl(
// Calculate canvas size
let decoder: PatternDecoder;
try {
decoder = new PatternDecoder(pattern, base64url.decode);
decoder = new PatternDecoder(
{
name: pattern.name,
patternData: pattern.patternData,
colorPalette: pattern.colorPalette,
},
base64url.decode,
);
} catch (e) {
console.error("Error decoding pattern", e);
return "";
@@ -201,14 +245,20 @@ function generatePreviewDataUrl(
// Create an image
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
const primary = pattern.colorPalette?.primaryColor
? new Colord(pattern.colorPalette.primaryColor).toRgb()
: DEFAULT_PRIMARY;
const secondary = pattern.colorPalette?.secondaryColor
? new Colord(pattern.colorPalette.secondaryColor).toRgb()
: DEFAULT_SECONDARY;
let i = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const rgba = decoder.isSet(x, y) ? COLOR_SET : COLOR_UNSET;
data[i++] = rgba[0]; // Red
data[i++] = rgba[1]; // Green
data[i++] = rgba[2]; // Blue
data[i++] = rgba[3]; // Alpha
const rgba = decoder.isPrimary(x, y) ? primary : secondary;
data[i++] = rgba.r;
data[i++] = rgba.g;
data[i++] = rgba.b;
data[i++] = 255; // Alpha
}
}
+2 -2
View File
@@ -188,8 +188,8 @@ export class AnimatedSpriteLoader {
const baseImage = this.animatedSpriteImageMap.get(fxType);
const config = ANIMATED_SPRITE_CONFIG[fxType];
if (!baseImage || !config) return null;
const territoryColor = theme.territoryColor(owner);
const borderColor = theme.borderColor(owner);
const territoryColor = owner.territoryColor();
const borderColor = owner.borderColor();
const spawnHighlightColor = theme.spawnHighlightColor();
const key = `${fxType}-${owner.id()}`;
let coloredCanvas: HTMLCanvasElement;
+2 -3
View File
@@ -171,10 +171,9 @@ export const getColoredSprite = (
customTerritoryColor?: Colord,
customBorderColor?: Colord,
): HTMLCanvasElement => {
const owner = unit.owner();
const territoryColor: Colord =
customTerritoryColor ?? theme.territoryColor(owner);
const borderColor: Colord = customBorderColor ?? theme.borderColor(owner);
customTerritoryColor ?? unit.owner().territoryColor();
const borderColor: Colord = customBorderColor ?? unit.owner().borderColor();
const spawnHighlightColor = theme.spawnHighlightColor();
const key = computeSpriteKey(unit, territoryColor, borderColor);
if (coloredSpriteCache.has(key)) {
+1 -1
View File
@@ -170,7 +170,7 @@ export class RailroadLayer implements Layer {
const owner = this.game.owner(tile);
const recipient = owner.isPlayer() ? owner : null;
const color = recipient
? this.theme.railroadColor(recipient)
? recipient.borderColor()
: new Colord({ r: 255, g: 255, b: 255, a: 1 });
this.context.fillStyle = color.toRgbString();
this.paintRailRects(this.context, x, y, railType);
@@ -1,3 +1,5 @@
import { extend } from "colord";
import a11yPlugin from "colord/plugins/a11y";
import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
@@ -16,6 +18,8 @@ import { ToggleStructureEvent } from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
extend([a11yPlugin]);
type ShapeType = "triangle" | "square" | "pentagon" | "octagon" | "circle";
class StructureRenderInfo {
@@ -344,7 +348,7 @@ export class StructureIconsLayer implements Layer {
const structureType = isConstruction ? constructionType! : unit.type();
const cacheKey = isConstruction
? `construction-${structureType}` + (renderIcon ? "-icon" : "")
: `${this.theme.territoryColor(unit.owner()).toRgbString()}-${structureType}` +
: `${unit.owner().territoryColor().toRgbString()}-${structureType}` +
(renderIcon ? "-icon" : "");
if (this.textureCache.has(cacheKey)) {
return this.textureCache.get(cacheKey)!;
@@ -381,18 +385,23 @@ export class StructureIconsLayer implements Layer {
structureCanvas.height = Math.ceil(iconSize);
const context = structureCanvas.getContext("2d")!;
const tc = owner.territoryColor();
const bc = owner.borderColor();
const darker = bc.luminance() < tc.luminance() ? bc : tc;
const lighter = bc.luminance() < tc.luminance() ? tc : bc;
let borderColor: string;
if (isConstruction) {
context.fillStyle = "rgb(198, 198, 198)";
borderColor = "rgb(128, 127, 127)";
} else {
context.fillStyle = this.theme
.territoryColor(owner)
context.fillStyle = lighter
.lighten(0.13)
.alpha(renderIcon ? 0.65 : 1)
.toRgbString();
const darken = this.theme.borderColor(owner).isLight() ? 0.17 : 0.15;
borderColor = this.theme.borderColor(owner).darken(darken).toRgbString();
const darken = darker.isLight() ? 0.17 : 0.15;
borderColor = darker.darken(darken).toRgbString();
}
context.strokeStyle = borderColor;
+3 -3
View File
@@ -190,7 +190,7 @@ export class StructureLayer implements Layer {
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() === UnitType.Construction
? underConstructionColor
: this.theme.territoryColor(unit.owner()),
: unit.owner().territoryColor(),
130,
);
}
@@ -203,7 +203,7 @@ export class StructureLayer implements Layer {
const config = this.unitConfigs[unitType];
let icon: HTMLImageElement | undefined;
let borderColor = this.theme.borderColor(unit.owner());
let borderColor = unit.owner().borderColor();
// Handle cooldown states and special icons
if (unit.type() === UnitType.Construction) {
@@ -244,7 +244,7 @@ export class StructureLayer implements Layer {
height: number,
unit: UnitView,
) {
let color = this.theme.borderColor(unit.owner());
let color = unit.owner().borderColor();
if (unit.type() === UnitType.Construction) {
color = underConstructionColor;
}
+14 -49
View File
@@ -12,7 +12,6 @@ import {
AlternateViewEvent,
DragEvent,
MouseOverEvent,
RefreshGraphicsEvent,
} from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
@@ -75,11 +74,6 @@ export class TerritoryLayer implements Layer {
}
tick() {
const prev = this.cachedTerritoryPatternsEnabled;
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
if (prev !== undefined && prev !== this.cachedTerritoryPatternsEnabled) {
this.eventBus.emit(new RefreshGraphicsEvent());
}
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
const updates = this.game.updatesSinceLastTick();
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
@@ -464,53 +458,24 @@ export class TerritoryLayer implements Layer {
const alternativeColor = this.alternateViewColor(owner);
this.paintTile(this.alternativeImageData, tile, alternativeColor, 255);
}
if (
this.game.hasUnitNearby(
tile,
this.game.config().defensePostRange(),
UnitType.DefensePost,
owner.id(),
)
) {
const borderColors = this.theme.defendedBorderColors(owner);
const x = this.game.x(tile);
const y = this.game.y(tile);
const lightTile =
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
const borderColor = lightTile ? borderColors.light : borderColors.dark;
this.paintTile(this.imageData, tile, borderColor, 255);
} else {
const useBorderColor = playerIsFocused
? this.theme.focusedBorderColor()
: this.theme.borderColor(owner);
this.paintTile(this.imageData, tile, useBorderColor, 255);
}
} else {
// Interior tiles
const pattern = owner.cosmetics.pattern;
const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false;
const isDefended = this.game.hasUnitNearby(
tile,
this.game.config().defensePostRange(),
UnitType.DefensePost,
owner.id(),
);
this.paintTile(
this.imageData,
tile,
owner.borderColor(tile, isDefended),
255,
);
} else {
// Alternative view only shows borders.
this.clearAlternativeTile(tile);
if (pattern === undefined || patternsEnabled === false) {
this.paintTile(
this.imageData,
tile,
this.theme.territoryColor(owner),
150,
);
} else {
const x = this.game.x(tile);
const y = this.game.y(tile);
const baseColor = this.theme.territoryColor(owner);
const decoder = owner.patternDecoder();
const color = decoder?.isSet(x, y)
? baseColor.darken(0.125)
: baseColor;
this.paintTile(this.imageData, tile, color, 150);
}
this.paintTile(this.imageData, tile, owner.territoryColor(tile), 150);
}
}
+2 -2
View File
@@ -143,7 +143,7 @@ export class UILayer implements Layer {
if (this.context === null || this.theme === null) {
return;
}
const color = this.theme.borderColor(unit.owner());
const color = unit.owner().borderColor();
this.context.fillStyle = color.toRgbString();
this.context.fillRect(startX, startY, icon.width, icon.height);
this.context.drawImage(icon, startX, startY);
@@ -208,7 +208,7 @@ export class UILayer implements Layer {
// Get the unit's owner color for the box
if (this.theme === null) throw new Error("missing theme");
const ownerColor = this.theme.territoryColor(unit.owner());
const ownerColor = unit.owner().territoryColor();
// Create a brighter version of the owner color for the selection
const selectionColor = ownerColor.lighten(0.2);
+7 -11
View File
@@ -201,7 +201,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
this.relationship(unit),
this.theme.territoryColor(unit.owner()),
unit.owner().territoryColor(),
150,
this.unitTrailContext,
);
@@ -321,14 +321,14 @@ export class UnitLayer implements Layer {
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner()),
unit.owner().borderColor(),
255,
);
this.paintCell(
this.game.x(unit.lastTile()),
this.game.y(unit.lastTile()),
rel,
this.theme.borderColor(unit.owner()),
unit.owner().borderColor(),
255,
);
}
@@ -369,7 +369,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(other.owner()),
other.owner().territoryColor(),
150,
this.unitTrailContext,
);
@@ -410,7 +410,7 @@ export class UnitLayer implements Layer {
this.drawTrail(
trail.slice(-newTrailSize),
this.theme.territoryColor(unit.owner()),
unit.owner().territoryColor(),
rel,
);
this.drawSprite(unit);
@@ -430,7 +430,7 @@ export class UnitLayer implements Layer {
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner()),
unit.owner().borderColor(),
255,
);
}
@@ -454,11 +454,7 @@ export class UnitLayer implements Layer {
trail.push(unit.lastTile());
// Paint trail
this.drawTrail(
trail.slice(-1),
this.theme.territoryColor(unit.owner()),
rel,
);
this.drawTrail(trail.slice(-1), unit.owner().territoryColor(), rel);
this.drawSprite(unit);
if (!unit.isActive()) {
+39 -10
View File
@@ -1,12 +1,16 @@
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { Pattern } from "../../../core/CosmeticSchemas";
import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import "../../components/PatternButton";
import { fetchPatterns, handlePurchase } from "../../Cosmetics";
import {
fetchCosmetics,
handlePurchase,
patternRelationship,
} from "../../Cosmetics";
import { getUserMe } from "../../jwt";
import { SendWinnerEvent } from "../../Transport";
import { Layer } from "./Layer";
@@ -108,19 +112,41 @@ export class WinModal extends LitElement implements Layer {
async loadPatternContent() {
const me = await getUserMe();
const patterns = await fetchPatterns(me !== false ? me : null);
const patterns = await fetchCosmetics();
const purchasable = Array.from(patterns.values()).filter(
(p) => p.product !== null,
);
const purchasablePatterns: {
pattern: Pattern;
colorPalette: ColorPalette;
}[] = [];
if (purchasable.length === 0) {
for (const pattern of Object.values(patterns?.patterns ?? {})) {
for (const colorPalette of pattern.colorPalettes ?? []) {
if (
patternRelationship(
pattern,
colorPalette,
me !== false ? me : null,
null,
) === "purchasable"
) {
const palette = patterns?.colorPalettes?.[colorPalette.name];
if (palette) {
purchasablePatterns.push({
pattern,
colorPalette: palette,
});
}
}
}
}
if (purchasablePatterns.length === 0) {
this.patternContent = html``;
return;
}
// Shuffle the array and take patterns based on screen size
const shuffled = [...purchasable].sort(() => Math.random() - 0.5);
const shuffled = [...purchasablePatterns].sort(() => Math.random() - 0.5);
const isMobile = window.innerWidth < 768; // md breakpoint
const maxPatterns = isMobile ? 1 : 3;
const selectedPatterns = shuffled.slice(
@@ -131,11 +157,14 @@ export class WinModal extends LitElement implements Layer {
this.patternContent = html`
<div class="flex gap-4 flex-wrap justify-start">
${selectedPatterns.map(
(pattern) => html`
({ pattern, colorPalette }) => html`
<pattern-button
.pattern=${pattern}
.colorPalette=${colorPalette}
.requiresPurchase=${true}
.onSelect=${(p: Pattern | null) => {}}
.onPurchase=${(p: Pattern) => handlePurchase(p)}
.onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
handlePurchase(p, colorPalette)}
></pattern-button>
`,
)}