From 3122052f6037713f9910e8ebec2e0aa2fa433935 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 30 Sep 2025 22:13:44 +0900 Subject: [PATCH 1/3] Cosmetic system overhaul: integrate cosmetic manifest handling and update sprite/icon loading logic --- .../cosmetic_pack/test/manifest.json | 29 ++++++++ .../cosmetic_pack/test/sprites/test.png | Bin 0 -> 1437 bytes .../cosmetic_pack/test/structure/test.png | Bin 0 -> 1513 bytes src/client/CosmeticPackLoader.ts | 62 +++++++++++++++++ src/client/graphics/SpriteLoader.ts | 49 ++++++++++---- src/client/graphics/layers/StructureLayer.ts | 63 ++++++++++++++++-- src/core/CosmeticSchemas.ts | 29 ++++++++ 7 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 resources/cosmetics/cosmetic_pack/test/manifest.json create mode 100644 resources/cosmetics/cosmetic_pack/test/sprites/test.png create mode 100644 resources/cosmetics/cosmetic_pack/test/structure/test.png create mode 100644 src/client/CosmeticPackLoader.ts diff --git a/resources/cosmetics/cosmetic_pack/test/manifest.json b/resources/cosmetics/cosmetic_pack/test/manifest.json new file mode 100644 index 0000000000..14fcdc56d1 --- /dev/null +++ b/resources/cosmetics/cosmetic_pack/test/manifest.json @@ -0,0 +1,29 @@ +{ + "id": "test", + "name": "Test", + "assets": { + "structure": { + "img": { + "port": "structure/test.png", + "city": "structure/test.png", + "factory": "structure/test.png", + "missilesilo": "structure/test.png", + "defensepost": "structure/test.png", + "samlauncher": "structure/test.png" + } + }, + "sprites": { + "transportship": "sprites/test.png", + "warship": "sprites/test.png", + "sammissile": "sprites/test.png", + "atombomb": "sprites/test.png", + "hydrogenbomb": "sprites/test.png", + "tradeship": "sprites/test.png", + "mirv": "sprites/test.png", + "engine": "sprites/test.png", + "carriage": "sprites/test.png", + "loadedcarriage": "sprites/test.png" + }, + "audio": {} + } +} diff --git a/resources/cosmetics/cosmetic_pack/test/sprites/test.png b/resources/cosmetics/cosmetic_pack/test/sprites/test.png new file mode 100644 index 0000000000000000000000000000000000000000..5bba75c4c89f44bcc8e90a63679c626af2f967d8 GIT binary patch literal 1437 zcmbVMJ8aWH75R>H!si^qfH1VmXv~{47>NaX4Q4j-{^Chtw`;2{Qk}gO= z48Yt0gv7wkfVy=6#K;5-OpG7~#8!y`mAgEw7L`{m*}k*C`~Ls?U-ybjS4K~qJs}9f zXko!wVejMo9vNoO_shEy-xL>1dG_vfI@{aZ@18u^W}6|pva~8(`t+f~5z)f6L=aA% z;`fm7=RGOwj;;O1!mc=;?LuLp$X@?ZHxF;iPGQgub+s=7cArMRdO*Sp3&q~baba7Yt|)l+O#Ba|dH@*C>( z_W=Vex~`krIMkOQNKGU(zrkem19B)jDYYZ4uHb|;VubS>%*`>bCIa&@hLpr5Lh6HD z6$dVhMn=&MaomN-54no-`_5noQfx~+ak6H~My{k!fyoRovJ;vGG_419iRV|^2cf1` z(sLlga%>)evcmBog?|Nm2vkTMK&IIbU=^!TSe3*yl3X|9iGNA04xw1TIVkno(dPcU8nL}QsVok#G;JiangC}=z-uut<%dyY; z3)YX!!@G`G1z7MCmalfdDOT~V{;KYa-9i8m@dCFc#50RMssGtOas+y`%^GH;e@J%^ zmXHcK z+(f~dEsZ?Pzj-w(Xd|y*KBaqGUxtf6?$__08ySCl2Yl_^_`I{O#Mi%1ZwbSv3v2Ume(;ncd(2 z_y7K1sFcslOg%KED9TJ})msxgkk6re#oK%LrR!pIDrc)JVt;+_($`;|xcSr58yg$l z-m{lpo7vSSKRZ1$KaUpOgE#0SUt9&j*3|dkmM@6)!r_nKKQ{m1J;|pZJuHL|@U`-~ zviRdSb#a+YR-eoi<-P~xb3l3Xtyx7m_-k0*?yADrffrFSrj4^weDG<^1{jrWa+7d-$6&YXI6>bazaz}Jl?IzS%qZ!*t30>J0 zW*(K^BxpIMn6tFXSZkE4%E)A3*}7?gIUkcSmR=mcO@?}yQ&*LV)1aeS#j1G{XCqra++leyAv%0CW7W;DDeyOCv0N4kO&4MiMtvFpeZzW-XQq z#nQUI6~2#3amH~>XvuR`;kF)z0SatdASSjnShtH>-Y%M&Q!50T2_36uk)mV6!bsj@ zWJe}`w`6tp)^cQa3HIM8%SruqC%tXGr!=_lL&kRqk5cCTHVIm9MRf)_j4b^EdL4qP$*>Sh1mZJj%Ui*X>2<546o z<6%Z)F~L$Xs; { + if (!packId || key === undefined) { + return fallback; + } + try { + const manifest = fetchManifest(packId); + if (!manifest) { + return fallback; + } + // Determine category and subKey from the first "/" only. + const firstSlash = key.indexOf("/"); + if (firstSlash === -1) { + return fallback; + } + const category = key.slice(0, firstSlash); + const subKey = key.slice(firstSlash + 1); + + const table = (manifest.assets as Record)[category]; + if (table) { + const parts = subKey.split("/"); + let current: any = table; + for (const part of parts) { + if (current === null) break; + current = current[part]; + } + if (typeof current === "string") { + return `/cosmetics/cosmetic_pack/${packId}/${current}`; + } + } + } catch (e) { + console.warn("[cosmetics] manifest load failed", e); + } + return fallback; +} diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 29d5b77914..27165e8daa 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -12,6 +12,7 @@ import warshipSprite from "../../../resources/sprites/warship.png"; import { Theme } from "../../core/configuration/Config"; import { TrainType, UnitType } from "../../core/game/Game"; import { UnitView } from "../../core/game/GameView"; +import { resolveCosmeticUrl } from "../CosmeticPackLoader"; // Can't reuse TrainType because "loaded" is not a type, just an attribute const TrainTypeSprite = { @@ -22,17 +23,31 @@ const TrainTypeSprite = { type TrainTypeSprite = (typeof TrainTypeSprite)[keyof typeof TrainTypeSprite]; -const SPRITE_CONFIG: Partial> = { - [UnitType.TransportShip]: transportShipSprite, - [UnitType.Warship]: warshipSprite, - [UnitType.SAMMissile]: samMissileSprite, - [UnitType.AtomBomb]: atomBombSprite, - [UnitType.HydrogenBomb]: hydrogenBombSprite, - [UnitType.TradeShip]: tradeShipSprite, - [UnitType.MIRV]: mirvSprite, - [TrainTypeSprite.Engine]: trainEngineSprite, - [TrainTypeSprite.Carriage]: trainCarriageSprite, - [TrainTypeSprite.LoadedCarriage]: trainLoadedCarriageSprite, +const SPRITE_CONFIG: Partial< + Record +> = { + [UnitType.TransportShip]: { + key: "sprites/transportship", + url: transportShipSprite, + }, + [UnitType.Warship]: { key: "sprites/warship", url: warshipSprite }, + [UnitType.SAMMissile]: { key: "sprites/sammissile", url: samMissileSprite }, + [UnitType.AtomBomb]: { key: "sprites/atombomb", url: atomBombSprite }, + [UnitType.HydrogenBomb]: { + key: "sprites/hydrogenbomb", + url: hydrogenBombSprite, + }, + [UnitType.TradeShip]: { key: "sprites/tradeship", url: tradeShipSprite }, + [UnitType.MIRV]: { key: "sprites/mirv", url: mirvSprite }, + [TrainTypeSprite.Engine]: { key: "sprites/engine", url: trainEngineSprite }, + [TrainTypeSprite.Carriage]: { + key: "sprites/carriage", + url: trainCarriageSprite, + }, + [TrainTypeSprite.LoadedCarriage]: { + key: "sprites/loadedcarriage", + url: trainLoadedCarriageSprite, + }, }; const spriteMap: Map = new Map(); @@ -43,14 +58,20 @@ export const loadAllSprites = async (): Promise => { const totalSprites = entries.length; let loadedCount = 0; + const packId: string | null = "test"; // TODO: wire from server/client selection + await Promise.all( - entries.map(async ([unitType, url]) => { + entries.map(async ([unitType, value]) => { const typedUnitType = unitType as UnitType | TrainTypeSprite; - if (!url || url === "") { - console.warn(`No sprite URL for ${typedUnitType}, skipping...`); + const key = value?.key; + const fallbackUrl = value?.url; + + if (!fallbackUrl) { + console.warn(`No sprite url for ${typedUnitType}, skipping...`); return; } + const url = await resolveCosmeticUrl(packId, key, fallbackUrl); try { const img = new Image(); diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index dbfaf5cb76..843dc57f10 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -1,19 +1,19 @@ import { colord, Colord } from "colord"; -import { Theme } from "../../../core/configuration/Config"; -import { EventBus } from "../../../core/EventBus"; -import { TransformHandler } from "../TransformHandler"; -import { Layer } from "./Layer"; - import cityIcon from "../../../../resources/images/buildings/cityAlt1.png"; import factoryIcon from "../../../../resources/images/buildings/factoryAlt1.png"; import shieldIcon from "../../../../resources/images/buildings/fortAlt3.png"; import anchorIcon from "../../../../resources/images/buildings/port1.png"; import missileSiloIcon from "../../../../resources/images/buildings/silo1.png"; import SAMMissileIcon from "../../../../resources/images/buildings/silo4.png"; +import { Theme } from "../../../core/configuration/Config"; +import { EventBus } from "../../../core/EventBus"; import { Cell, UnitType } from "../../../core/game/Game"; import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; +import { resolveCosmeticUrl } from "../../CosmeticPackLoader"; +import { TransformHandler } from "../TransformHandler"; +import { Layer } from "./Layer"; const underConstructionColor = colord({ r: 150, g: 150, b: 150 }); @@ -38,7 +38,7 @@ export class StructureLayer implements Layer { private tempContext: CanvasRenderingContext2D; // Configuration for supported unit types only - private readonly unitConfigs: Partial> = { + private unitConfigs: Partial> = { [UnitType.Port]: { icon: anchorIcon, borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, @@ -98,7 +98,8 @@ export class StructureLayer implements Layer { }; } - private loadIconData() { + private async loadIconData() { + await this.applyCosmeticIcons(); Object.entries(this.unitConfigs).forEach(([unitType, config]) => { this.loadIcon(unitType, config); }); @@ -122,6 +123,54 @@ export class StructureLayer implements Layer { this.redraw(); } + private async applyCosmeticIcons(): Promise { + const packSpec = "test"; + this.unitConfigs[UnitType.Port] = { + ...this.unitConfigs[UnitType.Port]!, + icon: await resolveCosmeticUrl( + packSpec, + "structure/img/port", + anchorIcon, + ), + }; + this.unitConfigs[UnitType.City] = { + ...this.unitConfigs[UnitType.City]!, + icon: await resolveCosmeticUrl(packSpec, "structure/img/city", cityIcon), + }; + this.unitConfigs[UnitType.Factory] = { + ...this.unitConfigs[UnitType.Factory]!, + icon: await resolveCosmeticUrl( + packSpec, + "structure/img/factory", + factoryIcon, + ), + }; + this.unitConfigs[UnitType.MissileSilo] = { + ...this.unitConfigs[UnitType.MissileSilo]!, + icon: await resolveCosmeticUrl( + packSpec, + "structure/img/missilesilo", + missileSiloIcon, + ), + }; + this.unitConfigs[UnitType.DefensePost] = { + ...this.unitConfigs[UnitType.DefensePost]!, + icon: await resolveCosmeticUrl( + packSpec, + "structure/img/defensepost", + shieldIcon, + ), + }; + this.unitConfigs[UnitType.SAMLauncher] = { + ...this.unitConfigs[UnitType.SAMLauncher]!, + icon: await resolveCosmeticUrl( + packSpec, + "structure/img/samlauncher", + SAMMissileIcon, + ), + }; + } + redraw() { console.log("structure layer redrawing"); this.canvas = document.createElement("canvas"); diff --git a/src/core/CosmeticSchemas.ts b/src/core/CosmeticSchemas.ts index a4bcd6762c..0f575848b1 100644 --- a/src/core/CosmeticSchemas.ts +++ b/src/core/CosmeticSchemas.ts @@ -94,3 +94,32 @@ export const DefaultPattern = { patternData: "AAAAAA", colorPalette: undefined, } satisfies PlayerPattern; + +const imageFile = z + .string() + .regex(/\.(png|webp|jpg|jpeg|gif)$/i, "Invalid image extension"); +const audioFile = z + .string() + .regex(/\.(ogg|mp3|wav|m4a)$/i, "Invalid audio extension"); + +const ImageAssetNode: z.ZodType = z.lazy(() => + z.union([imageFile, z.record(z.string(), ImageAssetNode)]), +); +const AudioAssetNode: z.ZodType = z.lazy(() => + z.union([audioFile, z.record(z.string(), AudioAssetNode)]), +); + +export const CosmeticManifestSchema = z + .object({ + id: z.string(), + name: z.string(), + assets: z + .object({ + structure: z.record(z.string(), ImageAssetNode).optional(), + sprites: z.record(z.string(), ImageAssetNode).optional(), + audio: z.record(z.string(), AudioAssetNode).optional(), + }) + .strict(), + }) + .strict(); +export type CosmeticManifest = z.infer; From 7d624f508bb607ae4feebe1b6537e82cfda5c636 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Wed, 1 Oct 2025 21:11:53 +0900 Subject: [PATCH 2/3] Add pack handling to lobby configuration and cosmetic loading --- src/client/ClientGameRunner.ts | 1 + src/client/CosmeticPackLoader.ts | 4 +-- src/client/Main.ts | 1 + src/client/SinglePlayerModal.ts | 2 ++ src/client/Transport.ts | 1 + src/client/graphics/SpriteLoader.ts | 6 ++--- src/client/graphics/layers/StructureLayer.ts | 27 +++++++++++++++----- src/client/graphics/layers/UnitLayer.ts | 14 ++++++++-- src/core/Schemas.ts | 2 ++ src/core/game/UserSettings.ts | 4 +++ src/server/Worker.ts | 1 + 11 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 376a5bcc26..df0dd932be 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -52,6 +52,7 @@ export interface LobbyConfig { serverConfig: ServerConfig; pattern: PlayerPattern | undefined; flag: string; + pack: string | undefined; playerName: string; clientID: ClientID; gameID: GameID; diff --git a/src/client/CosmeticPackLoader.ts b/src/client/CosmeticPackLoader.ts index 0cf5b5c25c..1ffdd35879 100644 --- a/src/client/CosmeticPackLoader.ts +++ b/src/client/CosmeticPackLoader.ts @@ -23,11 +23,11 @@ function fetchManifest(packId: string): CosmeticManifest | undefined { } export async function resolveCosmeticUrl( - packId: string | null, + packId: string | undefined, key: string | undefined, fallback: string, ): Promise { - if (!packId || key === undefined) { + if (packId === undefined || key === undefined) { return fallback; } try { diff --git a/src/client/Main.ts b/src/client/Main.ts index 9d4f1eba83..42cda33d32 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -516,6 +516,7 @@ class Client { this.flagInput === null || this.flagInput.getCurrentFlag() === "xx" ? "" : this.flagInput.getCurrentFlag(), + pack: this.userSettings.getSelectedPackId() ?? undefined, playerName: this.usernameInput?.getCurrentUsername() ?? "", token: getPlayToken(), clientID: lobby.clientID, diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 0b36b715a4..1f1ef1582e 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -448,6 +448,7 @@ export class SinglePlayerModal extends LitElement { selectedPattern ??= cosmetics ? (this.userSettings.getDevOnlyPattern() ?? null) : null; + const selectedPackId = this.userSettings.getSelectedPackId(); this.dispatchEvent( new CustomEvent("join-lobby", { @@ -466,6 +467,7 @@ export class SinglePlayerModal extends LitElement { ? "" : flagInput.getCurrentFlag(), pattern: selectedPattern ?? undefined, + pack: selectedPackId ?? undefined, }, }, ], diff --git a/src/client/Transport.ts b/src/client/Transport.ts index ee49f0c4c8..70755adafb 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -381,6 +381,7 @@ export class Transport { flag: this.lobbyConfig.flag, patternName: this.lobbyConfig.pattern?.name, patternColorPaletteName: this.lobbyConfig.pattern?.colorPalette?.name, + pack: this.lobbyConfig.pack, }, } satisfies ClientJoinMessage); } diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 27165e8daa..7b411ec85b 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -53,13 +53,13 @@ const SPRITE_CONFIG: Partial< const spriteMap: Map = new Map(); // preload all images -export const loadAllSprites = async (): Promise => { +export const loadAllSprites = async ( + packId: string | undefined, +): Promise => { const entries = Object.entries(SPRITE_CONFIG); const totalSprites = entries.length; let loadedCount = 0; - const packId: string | null = "test"; // TODO: wire from server/client selection - await Promise.all( entries.map(async ([unitType, value]) => { const typedUnitType = unitType as UnitType | TrainTypeSprite; diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 843dc57f10..2c900bfdbc 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -34,6 +34,8 @@ export class StructureLayer implements Layer { private context: CanvasRenderingContext2D; private unitIcons: Map = new Map(); private theme: Theme; + private packId: string | undefined; + private structureLoaded = false; private tempCanvas: HTMLCanvasElement; private tempContext: CanvasRenderingContext2D; @@ -117,6 +119,14 @@ export class StructureLayer implements Layer { if (unit === undefined) continue; this.handleUnitRendering(unit); } + if (!this.structureLoaded) { + const myPlayer = this.game.myPlayer(); + if (myPlayer) { + this.packId = myPlayer.cosmetics.pack; + this.loadIconData(); + this.structureLoaded = true; + } + } } init() { @@ -124,23 +134,26 @@ export class StructureLayer implements Layer { } private async applyCosmeticIcons(): Promise { - const packSpec = "test"; this.unitConfigs[UnitType.Port] = { ...this.unitConfigs[UnitType.Port]!, icon: await resolveCosmeticUrl( - packSpec, + this.packId, "structure/img/port", anchorIcon, ), }; this.unitConfigs[UnitType.City] = { ...this.unitConfigs[UnitType.City]!, - icon: await resolveCosmeticUrl(packSpec, "structure/img/city", cityIcon), + icon: await resolveCosmeticUrl( + this.packId, + "structure/img/city", + cityIcon, + ), }; this.unitConfigs[UnitType.Factory] = { ...this.unitConfigs[UnitType.Factory]!, icon: await resolveCosmeticUrl( - packSpec, + this.packId, "structure/img/factory", factoryIcon, ), @@ -148,7 +161,7 @@ export class StructureLayer implements Layer { this.unitConfigs[UnitType.MissileSilo] = { ...this.unitConfigs[UnitType.MissileSilo]!, icon: await resolveCosmeticUrl( - packSpec, + this.packId, "structure/img/missilesilo", missileSiloIcon, ), @@ -156,7 +169,7 @@ export class StructureLayer implements Layer { this.unitConfigs[UnitType.DefensePost] = { ...this.unitConfigs[UnitType.DefensePost]!, icon: await resolveCosmeticUrl( - packSpec, + this.packId, "structure/img/defensepost", shieldIcon, ), @@ -164,7 +177,7 @@ export class StructureLayer implements Layer { this.unitConfigs[UnitType.SAMLauncher] = { ...this.unitConfigs[UnitType.SAMLauncher]!, icon: await resolveCosmeticUrl( - packSpec, + this.packId, "structure/img/samlauncher", SAMMissileIcon, ), diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 4945969bed..a0a5105189 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -36,6 +36,8 @@ export class UnitLayer implements Layer { private unitToTrail = new Map(); private theme: Theme; + private packId: string | undefined = undefined; + private spritesLoaded = false; private alternateView = false; @@ -68,6 +70,15 @@ export class UnitLayer implements Layer { ?.[GameUpdateType.Unit]?.map((unit) => unit.id); this.updateUnitsSprites(unitIds ?? []); + + if (!this.spritesLoaded) { + const myPlayer = this.game.myPlayer(); + if (myPlayer) { + this.packId = myPlayer.cosmetics.pack; + loadAllSprites(this.packId); + this.spritesLoaded = true; + } + } } init() { @@ -75,8 +86,7 @@ export class UnitLayer implements Layer { this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); this.redraw(); - - loadAllSprites(); + loadAllSprites(this.packId); } /** diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 2e2fbc2ee2..5c54dc556e 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -390,6 +390,7 @@ export const PlayerCosmeticRefsSchema = z.object({ flag: FlagSchema.optional(), patternName: PatternNameSchema.optional(), patternColorPaletteName: z.string().optional(), + pack: z.string().optional(), }); export const PlayerPatternSchema = z.object({ @@ -400,6 +401,7 @@ export const PlayerPatternSchema = z.object({ export const PlayerCosmeticsSchema = z.object({ flag: FlagSchema.optional(), pattern: PlayerPatternSchema.optional(), + pack: z.string().optional(), }); export const PlayerSchema = z.object({ clientID: ID, diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index fe9f1deeb7..ce17026506 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -154,4 +154,8 @@ export class UserSettings { localStorage.setItem(PATTERN_KEY, patternName); } } + + getSelectedPackId(): string | undefined { + return localStorage.getItem("cosmeticPackId") ?? undefined; + } } diff --git a/src/server/Worker.ts b/src/server/Worker.ts index e565f7ab76..9e1d482742 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -490,6 +490,7 @@ export async function startWorker() { cosmetics: { flag: cosmetics.flag, pattern: pattern, + pack: cosmetics.pack, }, }; } From 114ae7fb64177931fafa3544209b839de3e9fd6b Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Sun, 5 Oct 2025 09:10:53 +0900 Subject: [PATCH 3/3] fixed --- .../cosmetic_pack/test/manifest.json | 29 ------- .../cosmetic_pack/test/sprites/test.png | Bin 1437 -> 0 bytes .../test/structure => images/test}/test.png | Bin src/client/ClientGameRunner.ts | 20 ++++- src/client/CosmeticPackLoader.ts | 66 +++----------- src/client/Main.ts | 32 ++++++- src/client/SinglePlayerModal.ts | 81 +++++++++++++++++- src/client/Transport.ts | 19 +++- src/client/graphics/SpriteLoader.ts | 29 +++---- src/client/graphics/layers/StructureLayer.ts | 67 +++------------ src/client/graphics/layers/UnitLayer.ts | 9 +- src/core/Schemas.ts | 43 +++++++++- src/core/game/UserSettings.ts | 64 +++++++++++++- src/server/Worker.ts | 47 +++++++++- 14 files changed, 340 insertions(+), 166 deletions(-) delete mode 100644 resources/cosmetics/cosmetic_pack/test/manifest.json delete mode 100644 resources/cosmetics/cosmetic_pack/test/sprites/test.png rename resources/{cosmetics/cosmetic_pack/test/structure => images/test}/test.png (100%) diff --git a/resources/cosmetics/cosmetic_pack/test/manifest.json b/resources/cosmetics/cosmetic_pack/test/manifest.json deleted file mode 100644 index 14fcdc56d1..0000000000 --- a/resources/cosmetics/cosmetic_pack/test/manifest.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "test", - "name": "Test", - "assets": { - "structure": { - "img": { - "port": "structure/test.png", - "city": "structure/test.png", - "factory": "structure/test.png", - "missilesilo": "structure/test.png", - "defensepost": "structure/test.png", - "samlauncher": "structure/test.png" - } - }, - "sprites": { - "transportship": "sprites/test.png", - "warship": "sprites/test.png", - "sammissile": "sprites/test.png", - "atombomb": "sprites/test.png", - "hydrogenbomb": "sprites/test.png", - "tradeship": "sprites/test.png", - "mirv": "sprites/test.png", - "engine": "sprites/test.png", - "carriage": "sprites/test.png", - "loadedcarriage": "sprites/test.png" - }, - "audio": {} - } -} diff --git a/resources/cosmetics/cosmetic_pack/test/sprites/test.png b/resources/cosmetics/cosmetic_pack/test/sprites/test.png deleted file mode 100644 index 5bba75c4c89f44bcc8e90a63679c626af2f967d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1437 zcmbVMJ8aWH75R>H!si^qfH1VmXv~{47>NaX4Q4j-{^Chtw`;2{Qk}gO= z48Yt0gv7wkfVy=6#K;5-OpG7~#8!y`mAgEw7L`{m*}k*C`~Ls?U-ybjS4K~qJs}9f zXko!wVejMo9vNoO_shEy-xL>1dG_vfI@{aZ@18u^W}6|pva~8(`t+f~5z)f6L=aA% z;`fm7=RGOwj;;O1!mc=;?LuLp$X@?ZHxF;iPGQgub+s=7cArMRdO*Sp3&q~baba7Yt|)l+O#Ba|dH@*C>( z_W=Vex~`krIMkOQNKGU(zrkem19B)jDYYZ4uHb|;VubS>%*`>bCIa&@hLpr5Lh6HD z6$dVhMn=&MaomN-54no-`_5noQfx~+ak6H~My{k!fyoRovJ;vGG_419iRV|^2cf1` z(sLlga%>)evcmBog?|Nm2vkTMK&IIbU=^!TSe3*yl3X|9iGNA04xw1TIVkno(dPcU8nL}QsVok#G;JiangC}=z-uut<%dyY; z3)YX!!@G`G1z7MCmalfdDOT~V{;KYa-9i8m@dCFc#50RMssGtOas+y`%^GH;e@J%^ zmXHcK z+(f~dEsZ?Pzj-w(Xd|y*KBaqGUxtf6?$__08ySCl2Yl_^_`I{O#Mi%1ZwbSv3v { - if (packId === undefined || key === undefined) { - return fallback; - } - try { - const manifest = fetchManifest(packId); - if (!manifest) { - return fallback; - } - // Determine category and subKey from the first "/" only. - const firstSlash = key.indexOf("/"); - if (firstSlash === -1) { - return fallback; - } - const category = key.slice(0, firstSlash); - const subKey = key.slice(firstSlash + 1); - - const table = (manifest.assets as Record)[category]; - if (table) { - const parts = subKey.split("/"); - let current: any = table; - for (const part of parts) { - if (current === null) break; - current = current[part]; - } - if (typeof current === "string") { - return `/cosmetics/cosmetic_pack/${packId}/${current}`; - } - } - } catch (e) { - console.warn("[cosmetics] manifest load failed", e); - } - return fallback; -} diff --git a/src/client/Main.ts b/src/client/Main.ts index 42cda33d32..b4f2d08c42 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -516,7 +516,37 @@ class Client { this.flagInput === null || this.flagInput.getCurrentFlag() === "xx" ? "" : this.flagInput.getCurrentFlag(), - pack: this.userSettings.getSelectedPackId() ?? undefined, + structurePort: + this.userSettings.getSelectedStructurePort() ?? undefined, + structureCity: + this.userSettings.getSelectedStructureCity() ?? undefined, + structureFactory: + this.userSettings.getSelectedStructureFactory() ?? undefined, + structureMissilesilo: + this.userSettings.getSelectedStructureMissilesilo() ?? undefined, + structureDefensepost: + this.userSettings.getSelectedStructureDefensepost() ?? undefined, + structureSamlauncher: + this.userSettings.getSelectedStructureSamlauncher() ?? undefined, + + spriteTransportship: + this.userSettings.getSelectedSpriteTransportship() ?? undefined, + spriteWarship: + this.userSettings.getSelectedSpriteWarship() ?? undefined, + spriteSammissile: + this.userSettings.getSelectedSpriteSammissile() ?? undefined, + spriteAtombomb: + this.userSettings.getSelectedSpriteAtombomb() ?? undefined, + spriteHydrogenbomb: + this.userSettings.getSelectedSpriteHydrogenbomb() ?? undefined, + spriteTradeship: + this.userSettings.getSelectedSpriteTradeship() ?? undefined, + spriteMirv: this.userSettings.getSelectedSpriteMirv() ?? undefined, + spriteEngine: this.userSettings.getSelectedSpriteEngine() ?? undefined, + spriteCarriage: + this.userSettings.getSelectedSpriteCarriage() ?? undefined, + spriteLoadedcarriage: + this.userSettings.getSelectedSpriteLoadedcarriage() ?? undefined, playerName: this.usernameInput?.getCurrentUsername() ?? "", token: getPlayToken(), clientID: lobby.clientID, diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 1f1ef1582e..48881c404f 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -21,6 +21,7 @@ import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; import "./components/Maps"; +import { fetchUrl } from "./CosmeticPackLoader"; import { fetchCosmetics } from "./Cosmetics"; import { FlagInput } from "./FlagInput"; import { JoinLobbyEvent } from "./Main"; @@ -448,7 +449,6 @@ export class SinglePlayerModal extends LitElement { selectedPattern ??= cosmetics ? (this.userSettings.getDevOnlyPattern() ?? null) : null; - const selectedPackId = this.userSettings.getSelectedPackId(); this.dispatchEvent( new CustomEvent("join-lobby", { @@ -467,7 +467,84 @@ export class SinglePlayerModal extends LitElement { ? "" : flagInput.getCurrentFlag(), pattern: selectedPattern ?? undefined, - pack: selectedPackId ?? undefined, + pack: { + structurePort: fetchUrl( + this.userSettings.getSelectedStructurePort() ?? undefined, + "structurePort", + ), + structureCity: fetchUrl( + this.userSettings.getSelectedStructureCity() ?? undefined, + "structureCity", + ), + structureFactory: fetchUrl( + this.userSettings.getSelectedStructureFactory() ?? + undefined, + "structureFactory", + ), + structureMissilesilo: fetchUrl( + this.userSettings.getSelectedStructureMissilesilo() ?? + undefined, + "structureMissilesilo", + ), + structureDefensepost: fetchUrl( + this.userSettings.getSelectedStructureDefensepost() ?? + undefined, + "structureDefensepost", + ), + structureSamlauncher: fetchUrl( + this.userSettings.getSelectedStructureSamlauncher() ?? + undefined, + "structureSamlauncher", + ), + + spriteTransportship: fetchUrl( + this.userSettings.getSelectedSpriteTransportship() ?? + undefined, + "spriteTransportship", + ), + spriteWarship: fetchUrl( + this.userSettings.getSelectedSpriteWarship() ?? undefined, + "spriteWarship", + ), + spriteSammissile: fetchUrl( + this.userSettings.getSelectedSpriteSammissile() ?? + undefined, + "spriteSammissile", + ), + spriteAtombomb: fetchUrl( + this.userSettings.getSelectedSpriteAtombomb() ?? + undefined, + "spriteAtombomb", + ), + spriteHydrogenbomb: fetchUrl( + this.userSettings.getSelectedSpriteHydrogenbomb() ?? + undefined, + "spriteHydrogenbomb", + ), + spriteTradeship: fetchUrl( + this.userSettings.getSelectedSpriteTradeship() ?? + undefined, + "spriteTradeship", + ), + spriteMirv: fetchUrl( + this.userSettings.getSelectedSpriteMirv() ?? undefined, + "spriteMirv", + ), + spriteEngine: fetchUrl( + this.userSettings.getSelectedSpriteEngine() ?? undefined, + "spriteEngine", + ), + spriteCarriage: fetchUrl( + this.userSettings.getSelectedSpriteCarriage() ?? + undefined, + "spriteCarriage", + ), + spriteLoadedcarriage: fetchUrl( + this.userSettings.getSelectedSpriteLoadedcarriage() ?? + undefined, + "spriteLoadedcarriage", + ), + }, }, }, ], diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 70755adafb..c5247c85a9 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -381,7 +381,24 @@ export class Transport { flag: this.lobbyConfig.flag, patternName: this.lobbyConfig.pattern?.name, patternColorPaletteName: this.lobbyConfig.pattern?.colorPalette?.name, - pack: this.lobbyConfig.pack, + + structurePort: this.lobbyConfig.structurePort, + structureCity: this.lobbyConfig.structureCity, + structureFactory: this.lobbyConfig.structureFactory, + structureMissilesilo: this.lobbyConfig.structureMissilesilo, + structureDefensepost: this.lobbyConfig.structureDefensepost, + structureSamlauncher: this.lobbyConfig.structureSamlauncher, + + spriteTransportship: this.lobbyConfig.spriteTransportship, + spriteWarship: this.lobbyConfig.spriteWarship, + spriteSammissile: this.lobbyConfig.spriteSammissile, + spriteAtombomb: this.lobbyConfig.spriteAtombomb, + spriteHydrogenbomb: this.lobbyConfig.spriteHydrogenbomb, + spriteTradeship: this.lobbyConfig.spriteTradeship, + spriteMirv: this.lobbyConfig.spriteMirv, + spriteEngine: this.lobbyConfig.spriteEngine, + spriteCarriage: this.lobbyConfig.spriteCarriage, + spriteLoadedcarriage: this.lobbyConfig.spriteLoadedcarriage, }, } satisfies ClientJoinMessage); } diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 7b411ec85b..aa3b438e99 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -12,7 +12,7 @@ import warshipSprite from "../../../resources/sprites/warship.png"; import { Theme } from "../../core/configuration/Config"; import { TrainType, UnitType } from "../../core/game/Game"; import { UnitView } from "../../core/game/GameView"; -import { resolveCosmeticUrl } from "../CosmeticPackLoader"; +import { PlayerPack } from "../../core/Schemas"; // Can't reuse TrainType because "loaded" is not a type, just an attribute const TrainTypeSprite = { @@ -27,25 +27,25 @@ const SPRITE_CONFIG: Partial< Record > = { [UnitType.TransportShip]: { - key: "sprites/transportship", + key: "spriteTransportship", url: transportShipSprite, }, - [UnitType.Warship]: { key: "sprites/warship", url: warshipSprite }, - [UnitType.SAMMissile]: { key: "sprites/sammissile", url: samMissileSprite }, - [UnitType.AtomBomb]: { key: "sprites/atombomb", url: atomBombSprite }, + [UnitType.Warship]: { key: "spriteWarship", url: warshipSprite }, + [UnitType.SAMMissile]: { key: "spriteSammissile", url: samMissileSprite }, + [UnitType.AtomBomb]: { key: "spriteAtombomb", url: atomBombSprite }, [UnitType.HydrogenBomb]: { - key: "sprites/hydrogenbomb", + key: "spriteHydrogenbomb", url: hydrogenBombSprite, }, - [UnitType.TradeShip]: { key: "sprites/tradeship", url: tradeShipSprite }, - [UnitType.MIRV]: { key: "sprites/mirv", url: mirvSprite }, - [TrainTypeSprite.Engine]: { key: "sprites/engine", url: trainEngineSprite }, + [UnitType.TradeShip]: { key: "spriteTradeship", url: tradeShipSprite }, + [UnitType.MIRV]: { key: "spriteMirv", url: mirvSprite }, + [TrainTypeSprite.Engine]: { key: "spriteEngine", url: trainEngineSprite }, [TrainTypeSprite.Carriage]: { - key: "sprites/carriage", + key: "spriteCarriage", url: trainCarriageSprite, }, [TrainTypeSprite.LoadedCarriage]: { - key: "sprites/loadedcarriage", + key: "spriteLoadedcarriage", url: trainLoadedCarriageSprite, }, }; @@ -53,9 +53,7 @@ const SPRITE_CONFIG: Partial< const spriteMap: Map = new Map(); // preload all images -export const loadAllSprites = async ( - packId: string | undefined, -): Promise => { +export const loadAllSprites = async (pack: PlayerPack): Promise => { const entries = Object.entries(SPRITE_CONFIG); const totalSprites = entries.length; let loadedCount = 0; @@ -71,7 +69,8 @@ export const loadAllSprites = async ( console.warn(`No sprite url for ${typedUnitType}, skipping...`); return; } - const url = await resolveCosmeticUrl(packId, key, fallbackUrl); + + const url = pack?.[key] ?? fallbackUrl; try { const img = new Image(); diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 2c900bfdbc..6a54306667 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -11,7 +11,7 @@ import { Cell, UnitType } from "../../../core/game/Game"; import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; -import { resolveCosmeticUrl } from "../../CosmeticPackLoader"; +import { PlayerPack } from "../../../core/Schemas"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; @@ -25,6 +25,7 @@ const ZOOM_THRESHOLD = 4.3; // below this zoom level, structures are not rendere interface UnitRenderConfig { icon: string; + key: string; borderRadius: number; territoryRadius: number; } @@ -34,7 +35,7 @@ export class StructureLayer implements Layer { private context: CanvasRenderingContext2D; private unitIcons: Map = new Map(); private theme: Theme; - private packId: string | undefined; + private pack: PlayerPack; private structureLoaded = false; private tempCanvas: HTMLCanvasElement; private tempContext: CanvasRenderingContext2D; @@ -43,31 +44,37 @@ export class StructureLayer implements Layer { private unitConfigs: Partial> = { [UnitType.Port]: { icon: anchorIcon, + key: "structurePort", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.City]: { icon: cityIcon, + key: "structureCity", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.Factory]: { icon: factoryIcon, + key: "structureFactory", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.MissileSilo]: { icon: missileSiloIcon, + key: "structureMissilesilo", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.DefensePost]: { icon: shieldIcon, + key: "structureDefensepost", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.SAMLauncher]: { icon: SAMMissileIcon, + key: "structureSamlauncher", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, @@ -88,6 +95,7 @@ export class StructureLayer implements Layer { private loadIcon(unitType: string, config: UnitRenderConfig) { const image = new Image(); + console.log(`loading icon for ${unitType} from ${config.icon}`); image.src = config.icon; image.onload = () => { this.unitIcons.set(unitType, image); @@ -101,8 +109,8 @@ export class StructureLayer implements Layer { } private async loadIconData() { - await this.applyCosmeticIcons(); Object.entries(this.unitConfigs).forEach(([unitType, config]) => { + config.icon = this.pack?.[config.key] ?? config.icon; this.loadIcon(unitType, config); }); } @@ -122,7 +130,7 @@ export class StructureLayer implements Layer { if (!this.structureLoaded) { const myPlayer = this.game.myPlayer(); if (myPlayer) { - this.packId = myPlayer.cosmetics.pack; + this.pack = myPlayer.cosmetics.pack ?? {}; this.loadIconData(); this.structureLoaded = true; } @@ -133,57 +141,6 @@ export class StructureLayer implements Layer { this.redraw(); } - private async applyCosmeticIcons(): Promise { - this.unitConfigs[UnitType.Port] = { - ...this.unitConfigs[UnitType.Port]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/port", - anchorIcon, - ), - }; - this.unitConfigs[UnitType.City] = { - ...this.unitConfigs[UnitType.City]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/city", - cityIcon, - ), - }; - this.unitConfigs[UnitType.Factory] = { - ...this.unitConfigs[UnitType.Factory]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/factory", - factoryIcon, - ), - }; - this.unitConfigs[UnitType.MissileSilo] = { - ...this.unitConfigs[UnitType.MissileSilo]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/missilesilo", - missileSiloIcon, - ), - }; - this.unitConfigs[UnitType.DefensePost] = { - ...this.unitConfigs[UnitType.DefensePost]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/defensepost", - shieldIcon, - ), - }; - this.unitConfigs[UnitType.SAMLauncher] = { - ...this.unitConfigs[UnitType.SAMLauncher]!, - icon: await resolveCosmeticUrl( - this.packId, - "structure/img/samlauncher", - SAMMissileIcon, - ), - }; - } - redraw() { console.log("structure layer redrawing"); this.canvas = document.createElement("canvas"); diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index a0a5105189..90c7faacf2 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -14,6 +14,7 @@ import { MoveWarshipIntentEvent } from "../../Transport"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; +import { PlayerPack } from "../../../core/Schemas"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { getColoredSprite, @@ -36,7 +37,7 @@ export class UnitLayer implements Layer { private unitToTrail = new Map(); private theme: Theme; - private packId: string | undefined = undefined; + private pack: PlayerPack; private spritesLoaded = false; private alternateView = false; @@ -74,8 +75,8 @@ export class UnitLayer implements Layer { if (!this.spritesLoaded) { const myPlayer = this.game.myPlayer(); if (myPlayer) { - this.packId = myPlayer.cosmetics.pack; - loadAllSprites(this.packId); + this.pack = myPlayer.cosmetics.pack ?? {}; + loadAllSprites(this.pack); this.spritesLoaded = true; } } @@ -86,7 +87,7 @@ export class UnitLayer implements Layer { this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); this.redraw(); - loadAllSprites(this.packId); + loadAllSprites(this.pack); } /** diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 5c54dc556e..210322b6a2 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -390,7 +390,24 @@ export const PlayerCosmeticRefsSchema = z.object({ flag: FlagSchema.optional(), patternName: PatternNameSchema.optional(), patternColorPaletteName: z.string().optional(), - pack: z.string().optional(), + + structurePort: z.string().optional(), + structureCity: z.string().optional(), + structureFactory: z.string().optional(), + structureMissilesilo: z.string().optional(), + structureDefensepost: z.string().optional(), + structureSamlauncher: z.string().optional(), + + spriteTransportship: z.string().optional(), + spriteWarship: z.string().optional(), + spriteSammissile: z.string().optional(), + spriteAtombomb: z.string().optional(), + spriteHydrogenbomb: z.string().optional(), + spriteTradeship: z.string().optional(), + spriteMirv: z.string().optional(), + spriteEngine: z.string().optional(), + spriteCarriage: z.string().optional(), + spriteLoadedcarriage: z.string().optional(), }); export const PlayerPatternSchema = z.object({ @@ -398,10 +415,32 @@ export const PlayerPatternSchema = z.object({ patternData: PatternDataSchema, colorPalette: ColorPaletteSchema.optional(), }); + +export const PlayerPackSchema = z.object({ + structurePort: z.string().optional(), + structureCity: z.string().optional(), + structureFactory: z.string().optional(), + structureMissilesilo: z.string().optional(), + structureDefensepost: z.string().optional(), + structureSamlauncher: z.string().optional(), + + spriteTransportship: z.string().optional(), + spriteWarship: z.string().optional(), + spriteSammissile: z.string().optional(), + spriteAtombomb: z.string().optional(), + spriteHydrogenbomb: z.string().optional(), + spriteTradeship: z.string().optional(), + spriteMirv: z.string().optional(), + spriteEngine: z.string().optional(), + spriteCarriage: z.string().optional(), + spriteLoadedcarriage: z.string().optional(), +}); +export type PlayerPack = z.infer; + export const PlayerCosmeticsSchema = z.object({ flag: FlagSchema.optional(), pattern: PlayerPatternSchema.optional(), - pack: z.string().optional(), + pack: PlayerPackSchema.optional(), }); export const PlayerSchema = z.object({ clientID: ID, diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index 9392703eb3..1635e5c186 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -169,8 +169,68 @@ export class UserSettings { } } - getSelectedPackId(): string | undefined { - return localStorage.getItem("cosmeticPackId") ?? undefined; + getSelectedStructurePort(): string | undefined { + return localStorage.getItem("structurePort") ?? undefined; + } + + getSelectedStructureCity(): string | undefined { + return localStorage.getItem("structureCity") ?? undefined; + } + + getSelectedStructureFactory(): string | undefined { + return localStorage.getItem("structureFactory") ?? undefined; + } + + getSelectedStructureMissilesilo(): string | undefined { + return localStorage.getItem("structureMissilesilo") ?? undefined; + } + + getSelectedStructureDefensepost(): string | undefined { + return localStorage.getItem("structureDefensepost") ?? undefined; + } + + getSelectedStructureSamlauncher(): string | undefined { + return localStorage.getItem("structureSamlauncher") ?? undefined; + } + + getSelectedSpriteTransportship(): string | undefined { + return localStorage.getItem("spriteTransportship") ?? undefined; + } + + getSelectedSpriteWarship(): string | undefined { + return localStorage.getItem("spriteWarship") ?? undefined; + } + + getSelectedSpriteSammissile(): string | undefined { + return localStorage.getItem("spriteSammissile") ?? undefined; + } + + getSelectedSpriteAtombomb(): string | undefined { + return localStorage.getItem("spriteAtombomb") ?? undefined; + } + + getSelectedSpriteHydrogenbomb(): string | undefined { + return localStorage.getItem("spriteHydrogenbomb") ?? undefined; + } + + getSelectedSpriteTradeship(): string | undefined { + return localStorage.getItem("spriteTradeship") ?? undefined; + } + + getSelectedSpriteMirv(): string | undefined { + return localStorage.getItem("spriteMirv") ?? undefined; + } + + getSelectedSpriteEngine(): string | undefined { + return localStorage.getItem("spriteEngine") ?? undefined; + } + + getSelectedSpriteCarriage(): string | undefined { + return localStorage.getItem("spriteCarriage") ?? undefined; + } + + getSelectedSpriteLoadedcarriage(): string | undefined { + return localStorage.getItem("spriteLoadedcarriage") ?? undefined; } backgroundMusicVolume(): number { diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 9e1d482742..a619119c9d 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -7,6 +7,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { WebSocket, WebSocketServer } from "ws"; import { z } from "zod"; +import { fetchUrl } from "../client/CosmeticPackLoader"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { GameType } from "../core/game/Game"; import { @@ -485,12 +486,56 @@ export async function startWorker() { } } + // Temporary full assignment (unless there’s a better approach). + const pack = { + structurePort: fetchUrl(cosmetics.structurePort, "structurePort"), + structureCity: fetchUrl(cosmetics.structureCity, "structureCity"), + structureFactory: fetchUrl( + cosmetics.structureFactory, + "structureFactory", + ), + structureMissilesilo: fetchUrl( + cosmetics.structureMissilesilo, + "structureMissilesilo", + ), + structureDefensepost: fetchUrl( + cosmetics.structureDefensepost, + "structureDefensepost", + ), + structureSamlauncher: fetchUrl( + cosmetics.structureSamlauncher, + "structureSamlauncher", + ), + spriteTransportship: fetchUrl( + cosmetics.spriteTransportship, + "spriteTransportship", + ), + spriteWarship: fetchUrl(cosmetics.spriteWarship, "spriteWarship"), + spriteSammissile: fetchUrl( + cosmetics.spriteSammissile, + "spriteSammissile", + ), + spriteAtombomb: fetchUrl(cosmetics.spriteAtombomb, "spriteAtombomb"), + spriteHydrogenbomb: fetchUrl( + cosmetics.spriteHydrogenbomb, + "spriteHydrogenbomb", + ), + spriteTradeship: fetchUrl(cosmetics.spriteTradeship, "spriteTradeship"), + spriteMirv: fetchUrl(cosmetics.spriteMirv, "spriteMirv"), + spriteEngine: fetchUrl(cosmetics.spriteEngine, "spriteEngine"), + spriteCarriage: fetchUrl(cosmetics.spriteCarriage, "spriteCarriage"), + spriteLoadedcarriage: fetchUrl( + cosmetics.spriteLoadedcarriage, + "spriteLoadedcarriage", + ), + }; + return { perm: "allowed", cosmetics: { flag: cosmetics.flag, pattern: pattern, - pack: cosmetics.pack, + pack: pack, }, }; }