mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 13:46:15 +00:00
Add crown cosmetic type (#4618)
## What Adds **crowns** as a new cosmetic category, wired end-to-end like flags/skins, plus a Crowns tab in the lobby cosmetics modal (#4617). Catalog shape in cosmetics.json: ```json "crowns": { "gold_crown": { "name": "gold_crown", "url": "...", "affiliateCode": null, "product": null, "priceHard": 5, "artist": "...", "rarity": "common" } } ``` ## Changes - **Core**: `CrownSchema` + optional `crowns` record in `CosmeticsSchema`; `crownName` in `PlayerCosmeticRefs`; resolved `crown: { name, url }` in `PlayerCosmetics` - **Server**: `isCrownAllowed` in the privilege checker — requires a `crown:*` or `crown:<name>` flare, resolves the ref to the catalog URL - **Client**: crown selection persisted under the `crown` localStorage key (stale/unowned selections self-clear), `crownRelationship` + resolve/refs wiring, `"crown"` purchase type, Crowns tab in the store, and a Crowns tab in the cosmetics modal (owned crowns + Default tile; selecting persists and keeps the modal open) - **i18n**: `store.crowns`, `store.no_crowns` - Tests for schema parsing, privilege checks, and cosmetic resolution ## Not in this PR - In-game rendering of `player.cosmetics.crown` (name pass / leaderboards / player panel) - API-side support (`/shop/purchase` accepting `cosmeticType: "crown"`, granting `crown:<name>` flares, serving `crowns` in cosmetics.json) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { PlayerPattern } from "./Schemas";
|
||||
export type Cosmetics = z.infer<typeof CosmeticsSchema>;
|
||||
export type Pattern = z.infer<typeof PatternSchema>;
|
||||
export type Flag = z.infer<typeof FlagSchema>;
|
||||
export type Crown = z.infer<typeof CrownSchema>;
|
||||
export type Skin = z.infer<typeof SkinSchema>;
|
||||
export type Pack = z.infer<typeof PackSchema>;
|
||||
export type Subscription = z.infer<typeof SubscriptionSchema>;
|
||||
@@ -96,6 +97,10 @@ export const FlagSchema = CosmeticSchema.extend({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const CrownSchema = CosmeticSchema.extend({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const SkinSchema = CosmeticSchema.extend({
|
||||
url: z.string(),
|
||||
});
|
||||
@@ -363,6 +368,7 @@ export const CosmeticsSchema = z.object({
|
||||
colorPalettes: z.record(z.string(), ColorPaletteSchema).optional(),
|
||||
patterns: z.record(z.string(), PatternSchema),
|
||||
flags: z.record(z.string(), FlagSchema),
|
||||
crowns: z.record(z.string(), CrownSchema).optional(),
|
||||
skins: z.record(z.string(), SkinSchema).optional(),
|
||||
// Grouped by effectType. Each effect also carries its own effectType (matching
|
||||
// this outer key) so an Effect stands alone and EffectSchema can discriminate
|
||||
|
||||
@@ -145,6 +145,7 @@ export type PlayerCosmeticRefs = z.infer<typeof PlayerCosmeticRefsSchema>;
|
||||
export type PlayerPattern = z.infer<typeof PlayerPatternSchema>;
|
||||
export type PlayerColor = z.infer<typeof PlayerColorSchema>;
|
||||
export type PlayerSkin = z.infer<typeof PlayerSkinSchema>;
|
||||
export type PlayerCrown = z.infer<typeof PlayerCrownSchema>;
|
||||
export type PlayerEffect = z.infer<typeof PlayerEffectSchema>;
|
||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||
export type GameInfo = z.infer<typeof GameInfoSchema>;
|
||||
@@ -653,6 +654,7 @@ export const PlayerCosmeticRefsSchema = z.object({
|
||||
patternName: CosmeticNameSchema.optional(),
|
||||
patternColorPaletteName: z.string().optional(),
|
||||
skinName: CosmeticNameSchema.optional(),
|
||||
crownName: CosmeticNameSchema.optional(),
|
||||
// One selected effect per slot: key = slot (effectType for trails, nukeType for
|
||||
// nuke explosions — see effectTypeForSlot), value = effect name.
|
||||
effects: z.record(z.string(), CosmeticNameSchema).optional(),
|
||||
@@ -663,6 +665,11 @@ export const PlayerSkinSchema = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const PlayerCrownSchema = z.object({
|
||||
name: CosmeticNameSchema,
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
// A resolved effect is just an identity: which effect, of which type. Its
|
||||
// attributes (the visual style) are resolved from the cosmetics catalog by
|
||||
// (effectType, name), so this needs no per-type variants — a new effectType
|
||||
@@ -678,6 +685,7 @@ export const PlayerCosmeticsSchema = z.object({
|
||||
pattern: PlayerPatternSchema.optional(),
|
||||
color: PlayerColorSchema.optional(),
|
||||
skin: PlayerSkinSchema.optional(),
|
||||
crown: PlayerCrownSchema.optional(),
|
||||
// Resolved effects keyed by slot (effectType for trails, nukeType for nuke
|
||||
// explosions).
|
||||
effects: z.record(z.string(), PlayerEffectSchema).optional(),
|
||||
|
||||
@@ -54,6 +54,7 @@ export const USER_SETTINGS_CHANGED_EVENT = "event:user-settings-changed";
|
||||
*/
|
||||
export const PATTERN_KEY = "territoryPattern";
|
||||
export const FLAG_KEY = "flag";
|
||||
export const CROWN_KEY = "crown";
|
||||
export const COLOR_KEY = "settings.territoryColor";
|
||||
export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay";
|
||||
export const KEYBINDS_KEY = "settings.keybinds";
|
||||
@@ -301,6 +302,19 @@ export class UserSettings {
|
||||
return data.startsWith(skinPrefix) ? data.slice(skinPrefix.length) : null;
|
||||
}
|
||||
|
||||
/** Returns the selected crown name, or null if none is selected. */
|
||||
getSelectedCrownName(): string | null {
|
||||
return this.getCached(CROWN_KEY);
|
||||
}
|
||||
|
||||
setSelectedCrownName(name: string | undefined): void {
|
||||
if (name === undefined) {
|
||||
this.removeCached(CROWN_KEY);
|
||||
} else {
|
||||
this.setCached(CROWN_KEY, name);
|
||||
}
|
||||
}
|
||||
|
||||
getFlag(): string | null {
|
||||
let flag = this.getCached(FLAG_KEY);
|
||||
if (!flag) return null;
|
||||
|
||||
Reference in New Issue
Block a user