mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 17:36:47 +00:00
cosmetic refactor (#3628)
## Description: The motivation is to have a single "cosmetic-button" element, so we can abstract out the cosmetic types. This will make it much easier to add new cosmetic types in the future. Unifies PatternButton and FlagButton into a single CosmeticButton component. Extracts a resolveCosmetics() function that flattens patterns × color palettes + flags into a ResolvedCosmetic[] with relationship status pre-computed, replacing duplicated resolution logic across four callers. * New CosmeticButton — renders patterns or flags based on ResolvedCosmetic.type * New resolveCosmetics() — centralizes ownership/purchase/blocked resolution * Extracted PatternPreview — canvas rendering split into its own module * Added type: "pattern" | "flag" discriminator to Zod cosmetic schemas * Deleted FlagButton.ts and PatternButton.ts * Added 320-line test suite for resolveCosmetics ## 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:
@@ -2,7 +2,7 @@ import type { TemplateResult } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { Cosmetics, Pattern } from "../core/CosmeticSchemas";
|
||||
import { Cosmetics } from "../core/CosmeticSchemas";
|
||||
import {
|
||||
PATTERN_KEY,
|
||||
USER_SETTINGS_CHANGED_EVENT,
|
||||
@@ -10,13 +10,15 @@ import {
|
||||
} from "../core/game/UserSettings";
|
||||
import { PlayerPattern } from "../core/Schemas";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/CosmeticButton";
|
||||
import "./components/NotLoggedInWarning";
|
||||
import "./components/PatternButton";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
import {
|
||||
fetchCosmetics,
|
||||
getPlayerCosmetics,
|
||||
patternRelationship,
|
||||
resolveCosmetics,
|
||||
ResolvedCosmetic,
|
||||
resolvedToPlayerPattern,
|
||||
} from "./Cosmetics";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
@@ -82,62 +84,39 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
}
|
||||
|
||||
private renderPatternGrid(): TemplateResult {
|
||||
const buttons: TemplateResult[] = [];
|
||||
const patterns: (Pattern | null)[] = [
|
||||
const items = resolveCosmetics(
|
||||
this.cosmetics,
|
||||
this.userMeResponse,
|
||||
null,
|
||||
...Object.values(this.cosmetics?.patterns ?? {}),
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
if (pattern === null && this.search) {
|
||||
continue;
|
||||
}
|
||||
if (pattern !== null && !this.includedInSearch(pattern.name)) {
|
||||
continue;
|
||||
}
|
||||
const colorPalettes = pattern
|
||||
? [...(pattern.colorPalettes ?? []), null]
|
||||
: [null];
|
||||
for (const colorPalette of colorPalettes) {
|
||||
let rel = "owned";
|
||||
if (pattern) {
|
||||
rel = patternRelationship(
|
||||
pattern,
|
||||
colorPalette,
|
||||
this.userMeResponse,
|
||||
null,
|
||||
);
|
||||
}
|
||||
if (rel !== "owned") {
|
||||
continue;
|
||||
}
|
||||
const isDefaultPattern = pattern === null;
|
||||
const isSelected =
|
||||
(isDefaultPattern && this.selectedPattern === null) ||
|
||||
(!isDefaultPattern &&
|
||||
this.selectedPattern &&
|
||||
this.selectedPattern.name === pattern?.name &&
|
||||
(this.selectedPattern.colorPalette?.name ?? null) ===
|
||||
(colorPalette?.name ?? null));
|
||||
buttons.push(html`
|
||||
<pattern-button
|
||||
.pattern=${pattern}
|
||||
.colorPalette=${this.cosmetics?.colorPalettes?.[
|
||||
colorPalette?.name ?? ""
|
||||
] ?? null}
|
||||
.requiresPurchase=${false}
|
||||
.selected=${isSelected}
|
||||
.onSelect=${(p: PlayerPattern | null) => this.selectPattern(p)}
|
||||
></pattern-button>
|
||||
`);
|
||||
}
|
||||
}
|
||||
).filter(
|
||||
(r) =>
|
||||
r.type === "pattern" &&
|
||||
r.relationship === "owned" &&
|
||||
(r.cosmetic === null
|
||||
? !this.search
|
||||
: this.includedInSearch(r.cosmetic.name)),
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-8 justify-center items-stretch content-start"
|
||||
>
|
||||
${buttons}
|
||||
${items.map((r) => {
|
||||
const isSelected =
|
||||
(r.cosmetic === null && this.selectedPattern === null) ||
|
||||
(r.cosmetic !== null &&
|
||||
this.selectedPattern?.name === r.cosmetic.name &&
|
||||
(this.selectedPattern?.colorPalette?.name ?? null) ===
|
||||
(r.colorPalette?.name ?? null));
|
||||
return html`
|
||||
<cosmetic-button
|
||||
.resolved=${r}
|
||||
.selected=${isSelected}
|
||||
.onSelect=${(rc: ResolvedCosmetic) => this.selectCosmetic(rc)}
|
||||
></cosmetic-button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -213,6 +192,11 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
this.search = "";
|
||||
}
|
||||
|
||||
private selectCosmetic(resolved: ResolvedCosmetic) {
|
||||
if (resolved.type !== "pattern") return;
|
||||
this.selectPattern(resolvedToPlayerPattern(resolved));
|
||||
}
|
||||
|
||||
private selectPattern(pattern: PlayerPattern | null) {
|
||||
this.selectedColor = null;
|
||||
if (pattern === null) {
|
||||
|
||||
Reference in New Issue
Block a user