mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 05:55:20 +00:00
95840a207429ff99b39d901cd4059c3ac3d981e9
20
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a639a6aa6b |
Gate public lobby listing on the canCreatePublicLobbies entitlement (#4620)
## What The API now returns an explicit `canCreatePublicLobbies` boolean on `/users/@me` (player) and on each subscription tier in `cosmetics.json`. This PR gates public lobby listing on that entitlement instead of inferring it from subscription status (`active`/`trialing`), and advertises the perk on the store's subscription tiles. ## Changes **Entitlement check** - `UserMeResponseSchema` gains a required `canCreatePublicLobbies` boolean (same pattern as `unlimitedRanked`) - The worker's listing endpoint and `HostLobbyModal`'s Public toggle both check the boolean directly; the Dev bypass is unchanged - `hasActiveSubscription()` is removed — these were its only call sites - The server error stays `subscription_required` (subscribing is still how you get the entitlement; renaming would ripple through i18n for no behavior change) **Store** - `SubscriptionSchema` (cosmetics catalog) gains a required `canCreatePublicLobbies` boolean - Subscription tiles show a "Create public lobbies" badge below "Unlimited ranked" when the tier grants it (`cosmetics.public_lobbies` added to en.json) **Tests** - Schema tests for both new fields (accepts true, rejects missing, rejects non-boolean); `UserMeResponse` fixtures updated ## Deploy ordering Both new schema fields are required, so this client must deploy after the API serves them (same constraint as `unlimitedRanked` / infra#427). The API already returns both fields. ## Verified Typecheck, ESLint, and the affected test files pass (165 tests). Confirmed the live payloads against a local API: `/users/@me` returns `player.canCreatePublicLobbies` and both subscription tiers in `cosmetics.json` carry the field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e05dce3437 |
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> |
||
|
|
376061793a |
Handle ranked play limits in the client (#4602)
## Summary
Client-side handling for the new server-enforced ranked play limits
(infra#427): free players get a lifetime allowance of ranked matches,
then a small daily allowance; subscribers on tiers with the
`unlimitedRanked` entitlement are never limited.
- **Queue rejection**: the matchmaking socket closing with code 1008 and
reason `ranked_limit_reached` now renders a limit-reached view in the
matchmaking modal (message + "Get unlimited ranked" button that opens
the store on the subscriptions tab) and does **not** auto-reconnect.
Auth-failure 1008 (`Invalid session`) is disambiguated by the reason
string and keeps the existing reconnect-with-refreshed-token path; code
1000 ("replaced by another tab") is unchanged. Both 1v1 and 2v2 go
through the same modal, so both queues get identical handling.
- **`/users/@me`**: added required `player.unlimitedRanked: boolean` to
`UserMeResponseSchema`.
- **cosmetics.json**: added required `unlimitedRanked: boolean` to
`SubscriptionSchema`; store subscription tiles now show an "Unlimited
ranked" perk line for tiers that include it.
Display copy avoids hardcoding the limit numbers since the server
constants may be tuned.
## ⚠️ Deploy ordering
Both new schema fields are **required**, so this must ship **after** the
API starts serving them (infra#427). Against the current API,
`/users/@me` parsing fails (players appear logged out) and a catalog
without the subscription field fails the whole cosmetics parse.
## Testing
- Schema tests for both new fields (present / rejected-when-missing) in
`tests/ApiSchemas.test.ts` and `tests/CosmeticSchemas.test.ts`; full
suite, tsc, ESLint, Prettier all pass.
- Verified in the running app (headless Chromium): forced the modal into
the limit-reached state, confirmed the rendered view, and confirmed the
upsell click closes matchmaking and opens the store on the subscriptions
tab.
- Not verifiable locally (needs deployed infra#427): the real close
frame, the midnight-UTC reset, and the perk line against a real catalog
— covered by the handoff QA checklist.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a794eca4d6 |
Show USD-equivalent value in cosmetic info tooltip (#4571)
## Summary fixes #4171 The question-mark info tooltip on store cosmetics now shows the item's equivalent USD value, but only when both: 1. The item has **no product** (i.e. it can't be purchased directly with money), and 2. It **is purchasable with plutonium** (`priceHard`) — caps-only items show nothing. The value is computed at the fixed rate of 20 plutonium = $1.00 (same rate as the custom currency card) and rendered as e.g. `Value: $2.50`. ## Changes - `CosmeticButton.ts` — compute `usdValue` (`priceHard / 20`) when the item has no product and has a plutonium price; pass it to `<cosmetic-info>` - `CosmeticInfo.ts` — new optional `usdValue` property rendered as a tooltip line between the ad-free and color lines - `en.json` — new `cosmetics.usd_value` key with a `{usd}` placeholder so translators can position the amount 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
be77ab4fc9 |
feat: structures cosmetic effect (hover-shown gradient/transition recolor) (#4492)
## Description: Adds a new `structures` cosmetic effect type: an equippable effect that recolors the owner's structure icons (City, Port, Factory, Defense Post, SAM Launcher, Missile Silo) with gradient or transition color styles. The effect is **shown while the owner's territory is hovered** — structures otherwise keep their normal player colors, so the map stays readable. **Cosmetics / selection** - `StructuresEffectAttributesSchema` (`CosmeticSchemas.ts`): its own discriminated union (`gradient` / `transition`) — structurally identical to the trail attributes today, but structures aren't trails, so it's a separate schema free to diverge. - Slot = the effectType itself: `effectTypeForSlot` is generalized to map any non-nukeExplosion effect type to itself, so server privilege checks (`Privilege.ts`), client selection, and persistence all work with no per-type code. - Effects tab, Default tile, and the store preview (shared color swatch) come from `EFFECT_TYPES`; the only UI addition is the `effects.type.structures` label in `en.json`. **Rendering** - The shared per-player effect palette grows from 2 to 3 blocks (`EFFECT_PALETTE_BLOCKS`; structures = block 2, pinned by a build-breaking guard). `syncPlayerEffects` resolves the `structures` selection through the same `writeEffectEntry` used by trails. - `StructurePass` binds the effect texture plus `uTime` and `uHoverOwner` (fed from the existing `HoverHighlightController` → `setHighlightOwner` path, now forwarded to the pass). - `structure.frag.glsl` recolors the **fill only** — the border keeps the player color for ownership legibility; alt view and construction gray bypass the effect entirely. - Style semantics: - `gradient` — the palette spans each icon's diagonal once (a visible gradient across the shape), sliding one full cycle every `colorSize · 4 · count / movementSpeed` seconds (the trail-equivalent pace; world-space banding like the trail's would put a whole icon inside one band and read as a flat color) - `transition` — the whole icon is one color at a time, cross-fading at `frequency` colors/s - Glyph contrast: the inner icon's black/white decision is now a smooth luminance fade (`smoothstep(0.25, 0.45)`) instead of a hard flip at 0.25, so animated fills cross-fade the glyph instead of snapping it between black and white. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b6317964a7 |
feat: sparkles nuke-explosion visual type (#4490)
## Description: Follow-up to #4485: adds a second nuke-explosion visual, `"sparkles"` — a firework burst of twinkling glints that start at the detonation point and ride outward with the expanding front, reaching the cosmetic's full `size` at fade-out. **Schema (`CosmeticSchemas.ts`)** - `NukeExplosionAttributesSchema` is now a discriminated union on `type` (`"shockwave" | "sparkles"`), matching `TrailEffectAttributesSchema`. Old clients drop sparkles entries via `lenientRecord` and render the default ring. - The sparkles member adds `density` (required, positive) — roughly the total number of glints in the burst. - Literal attribute semantics, consistent with shockwave: - `size` — final burst width (diameter) in world tiles at fade-out - `speed` — tiles/s the width grows; duration = size / speed, clamped 0.1–15 s - `thickness` — **average** sparkle size in tiles; each glint hash-varies ±50% around it - `density` — approximate glint count; renderer clamps to 2–5000 - `colors` + `transitionSpeed` — shared palette-cycle semantics, with a hashed per-glint palette offset on top **Rendering** - `NukeExplosionRenderParams` now carries the visual type through to the pass as a matching TS union (previously any cosmetic was hardwired to the EMP style — this closes that gap for future visuals). - Sparkles are style 2 in the same `FxShockwavePass` instance stream: one new float (grid cell pitch, derived CPU-side from density), no other layout changes. - Fragment shader: one hashed glint per rotated front-normalized grid cell (jittered, cell-confined so each fragment samples only its own cell, ~1/3 dropout for organic scatter), hashed birth stagger. Glints are **fully opaque** — twinkle modulates color brightness, not alpha — holding full opacity through life and fading only over the last quarter. - SAM interceptions, the classic ring, and EMP shockwaves are unchanged. **Store / selection** - New `<sparkles-swatch>` preview (burst scales from center, density-scaled dot count, size-varied dots, palette cycling), branched in `CosmeticButton` by `attributes.type`. **Verification** - Schema tests incl. the real `rgb_nuke_sparkles` catalog entry, missing/non-positive `density` rejection. - Verified in-game via headless Chromium: size-250 RGB burst renders opaque red/white/blue glints expanding from the detonation point; sparse (40) vs dense (400) density comparison; no page errors. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6ff202afb5 |
feat: nuke-explosion cosmetic effects (per-bomb-type shockwave customization) (#4485)
## Description: Adds a new `nukeExplosion` cosmetic effect type: when a bomb detonates, every client renders the shockwave in the firing player's equipped effect for that bomb type. **Cosmetics / selection** - New `nukeExplosion` effect schema (`CosmeticSchemas.ts`) with per-bomb selection slots — a slot is the effectType for trails and the `nukeType` for explosions (`atom` / `hydro` / `mirvWarhead`), so players can equip a distinct explosion per bomb type. - Slot resolution + validation is one shared helper (`findEffectForSlot`) used by client selection, server privilege checks (`Privilege.ts`), and the renderer; a compile-time guard keeps the nukeType and effectType slot namespaces disjoint. - Effects picker gains an Atom / Hydrogen / MIRV sub-tab bar when browsing nuke explosions; selections persist per slot in UserSettings and are validated/dropped like other cosmetics. **Rendering** - `WebGLFrameBuilder` resolves each dead nuke's owner cosmetic onto the dead-unit event; `FxShockwavePass` renders an EMP-style procedural ring (jagged crackling front, rotating lightning arcs, inner energy fill) from per-instance attributes. SAM interceptions and players with no cosmetic keep the classic white ring. - Catalog attributes have literal units: - `size` — final ring width (diameter) in world tiles at fade-out, absolute — independent of the bomb's blast radius - `speed` — tiles/s the width grows; duration = size / speed, clamped to 0.1–15 s - `thickness` (required) — ring band thickness in tiles, constant while the ring expands - `colors` — palette of up to 4 colors, cycled at `transitionSpeed` steps/s (0 = static, negative = reverse; same semantics as trail transitions) - The shockwave quad is sized radius + thickness so the absolute-width band isn't clipped into a box while the ring is young. ## Please complete the following: - [ ] 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2794ab1270 |
feat: nuke-trail cosmetic effect + tabbed effects picker (#4466)
## What Adds a **`nukeTrail`** cosmetic effectType alongside `transportShipTrail`, so nukes leave a trail colored by their own gradient/transition effect — independent of the boat-trail effect (a player can run both). Also reorganizes the effects picker and store into per-effectType **tabs**. ## Rendering Boat and nuke trails are stamped into **one** trail texture keyed only by owner, so independent coloring needs a per-tile unit-class signal: - **Trail texture** `R8UI` → `R16UI`: texel = `ownerID(bits 0-11) | nukeBit(bit 12)`. `TrailManager` stamps the bit (and preserves it when repainting on unit death); the `Uint8Array`→`Uint16Array` ripple + `UNSIGNED_SHORT` uploads flow through `GpuResources`, `TrailPass`, `Upload`, `MapRenderer`, `Renderer`, `FrameData`. - **Effect texture** widened to two stacked blocks (`TRAIL_EFFECT_BLOCKS`): rows 0–7 = transportShipTrail, rows 8–15 = nukeTrail. `writeEffectEntry(…, rowBase)`; `syncPlayerEffects` resolves both effectTypes. - **Shader** masks the owner, derives `rowBase` from the nuke bit, offsets every row, and reuses the gradient/transition decode. - Bonus: the 12-bit owner mask lifts the old `R8UI` >255-player truncation. ## Schema / server / UI - Shared attributes schema renamed `TransportShipTrail…` → **`TrailEffectAttributesSchema`** (it's no longer ship-specific); `NukeTrailEffectSchema` added to `EffectSchema` + `CosmeticsSchema.effects`. `EFFECT_TYPES = [transportShipTrail, nukeTrail]`. - Server `Privilege`, selection, and the picker grid all iterate `EFFECT_TYPES`, so they handle the new type with **no per-type code**. - **Tabs:** the selection modal uses one tab per effectType (`BaseModal`'s native tabs); the **store's** EFFECTS panel gets an internal sub-tab bar (its top-level PACKS/EFFECTS tabs can't nest). Tabs are always present, so a type you own entirely still appears as an empty tab (previously the boat-trail section vanished from the store when you owned everything). ## Review A 3-angle adversarial review (bit-packing, type-ripple, GLSL/data-flow) **refuted** the correctness concerns — the R16UI format, masking, and block layout agree across `TrailManager` / shader / builder. The minor survivors (a preview that only resolved boat trails, stale comments) were fixed. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` — all clean. - Schema/`Privilege` tests updated for `nukeTrail` (96 tests pass). - The GL trail + tab UI are visual — not yet verified in a running game. - The catalog (`cosmetics.json`, closed-source API) must ship the `effects.nukeTrail` block for the effect to appear in production. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
200f276ab2 |
feat: transport-ship trail transition effect + animated store swatch (#4455)
## What Adds a second transport-ship trail style, **transition**, alongside the existing **gradient** (#4454). Where `gradient` paints a spatial band of colors along the trail, `transition` makes the whole trail one color at a time, cross-fading through the color list over time. ```json "attributes": { "type": "transition", "colors": ["#002aff", "#4805ff"], "frequency": 1 } ``` ## How - **Schema** ([CosmeticSchemas.ts](src/core/CosmeticSchemas.ts)) — `TransportShipTrailAttributesSchema` is now a discriminated union on `type`: - `gradient`: `{ colors, colorSize, movementSpeed }` - `transition`: `{ colors, frequency }` — `frequency` = color changes per second. - **Renderer** — the effect texture gained a `styleId` discriminator (row 1's alpha; 0 = gradient, 1 = transition), with the gradient scalars shifted down a row. - [WebGLFrameBuilder.ts](src/client/WebGLFrameBuilder.ts) encodes `styleId` + the style's scalars. - [trail.frag.glsl](src/client/render/gl/shaders/map-overlay/trail.frag.glsl): for `transition`, the trail color is `mix(colors[i], colors[i+1], fract(t))` with `i = floor(uTime · frequency) mod count` — one color step every `1/frequency` seconds. - **Store/picker swatch** ([EffectPreview.ts](src/client/components/EffectPreview.ts)) — the swatch is now a `<trail-swatch>` Lit element. For `transition` it cross-fades through the colors via the Web Animations API, timed to match the shader (each step `1/frequency` s); gradient/solid stay static. The animation is canceled on disconnect. ## Notes - Animation is render-only (local time) — no simulation/determinism impact. - `gradient` swatches remain static (they don't scroll like the in-game trail) — easy to add later if wanted. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` all clean. - Schema tests cover the transition member (parse + required `frequency`); 95 tests pass. - The animated swatch is visual-only (no automated coverage) and not yet verified in a running store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a884eba1b |
store popup (#4435)
## Description: change the generic popup: <img width="1095" height="540" alt="image" src="https://github.com/user-attachments/assets/94d2c120-5ec5-4838-b8b4-09d43b4e83f8" /> into a popup i added for clan system: <img width="1108" height="774" alt="image" src="https://github.com/user-attachments/assets/d7de1666-7667-4422-a1bd-03b90b4ff8ab" /> caps doesn't have a "buy" button: <img width="1141" height="803" alt="image" src="https://github.com/user-attachments/assets/d26dd397-1f14-4963-8ac8-afa5f32ed8ec" /> also works for win modal: <img width="1023" height="766" alt="image" src="https://github.com/user-attachments/assets/83f7bc87-0ecc-4470-b84d-c5783560d6a3" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bd9ef9a317 |
feat: effects cosmetic category (transport-ship trail) + UI (#4418)
## What Adds a new **`effects`** cosmetic category alongside `skins`/`flags`. Each effect is discriminated by **`effectType`** (only `transportShipTrail` today), whose visual config lives in **`attributes`** (`solid` / `rainbow` / `pulse` / `gradient`). Schema matches the production cosmetics.json shape exactly (incl. the `url` field). **This PR is UI + taxonomy only — the in-game WebGL trail rendering is intentionally deferred.** ## UI - **Store** gains an **"Effects"** tab. - **Home page** gains an **"Effects"** button opening a picker modal. - Both render effects **grouped by `effectType` with a sub-header per type**, via a shared `<effects-grid>` Lit element (`mode="select"` for the picker, `mode="purchase"` for the store). The picker shows owned effects + a Default tile and persists per-type; the store shows purchasable effects. ## Data flow - Ownership via `effect:*` / `effect:<name>` flares (reuses `cosmeticRelationship`). - Selection is a per-`effectType` map persisted in UserSettings (`settings.effects`). - Server validates in `isEffectAllowed`, wired into `isAllowed`. - `getPlayerCosmeticsRefs` / `getPlayerCosmetics` resolve effects the same way as skins/flags (kept-on-fetch-failure, server is authority). ## Tests - `tsc --noEmit`, ESLint, Prettier clean; full suite green. - New: `CosmeticSchemas` parse tests (incl. parsing the **real** `read_transport_trail` entry), `UserSettings` per-type selection, and `Privilege` effect validation. ## Notes / follow-ups - The effect's display label shows **"Boat Trail"** for the `transportShipTrail` type (friendlier than the id). - Closed-source API gap: `/shop/purchase` (`purchaseWithCurrency`) needs to learn `"effect"` for **currency** purchase of effects; the **dollar/product** purchase path already works. Client types were widened accordingly. - In-game wake rendering can be ported from #4416. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
966dcf47a5 |
removes the "subscribe" and "purchase" text (#4436)
## Description: removes the "subscribe" and "purchase" text: <img width="988" height="496" alt="image" src="https://github.com/user-attachments/assets/9566b450-0943-4684-8321-8024422bbd96" /> <img width="993" height="505" alt="image" src="https://github.com/user-attachments/assets/f26bfd99-661d-48e3-beb3-e3e5d212e6f1" /> <img width="1002" height="511" alt="image" src="https://github.com/user-attachments/assets/c8f2aadf-15d1-4e29-8142-f684c6e492f0" /> this is what it looks like if you're subbed to something now: <img width="997" height="491" alt="image" src="https://github.com/user-attachments/assets/5f011213-7ced-4a64-860e-45a6b0a7418f" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n |
||
|
|
c622e8581c |
collapse skins under one banner (#4432)
## Description: merges skins under one item with a circular button below it: <img width="877" height="647" alt="image" src="https://github.com/user-attachments/assets/a405ba34-a970-4e8c-9287-fe0055d6a02e" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n |
||
|
|
712b2bc473 |
Show bonus amount on currency packs (#3907)
Show bonus amount on currency packs - Add `bonusAmount` field to `PackSchema` (non-negative int) - Render a rotated green corner ribbon (`+X FREE!`) on pack tiles when `bonusAmount > 0` - Add `cosmetics.free` translation key with `numFree` param <img width="720" height="359" alt="Screenshot 2026-05-12 at 7 40 12 AM" src="https://github.com/user-attachments/assets/3dd70fc4-c922-47f4-aee6-055047b58563" /> Describe the PR. - [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 regression is found: evan |
||
|
|
aa3959bffe |
feat: territory png based skins (#4006)
## Description: Add image-based territory skins as a new cosmetic type, rendered alongside the existing 1-bit patterns. Skins render a single PNG centered on each player's spawn tile — opaque pixels show the skin (multiplied by team color in team games, raw colors in FFA), transparent pixels and tiles outside the image bounds fall through to the regular player palette color. **Cosmetic plumbing** - `SkinSchema` in `CosmeticSchemas.ts`, optional `skins` map on `CosmeticsSchema` - `PlayerSkin`, `PlayerCosmetics.skin`, `PlayerCosmeticRefs.skinName` in `Schemas.ts` - Server-side resolution: `PrivilegeCheckerImpl.isSkinAllowed` (gated by `skin:*` / `skin:<name>` flares) - Client persistence: stored under `PATTERN_KEY` (`pattern:` and `skin:` share one slot — they're mutually exclusive) - `getPlayerCosmeticsRefs` only emits a `skinName` when cosmetics are loaded, the skin exists in the catalog, and the user has the right flare — otherwise drops the ref and clears storage **Renderer** - `SkinAtlasArray` — fixed `TEXTURE_2D_ARRAY`, 1024×1024 per layer, exact layer count allocated once at game start from the locked-in player set. No resize, no callbacks, no retained `HTMLImageElement`. Zero GPU cost when no players have skins (1×1 placeholder). - `skinLayerTex` (R8UI 4096×1) — per-player `layer + 1` (`0` = no skin) - `skinAnchorTex` (RG16UI 4096×1) — per-player spawn tile, so the PNG center anchors at each player's spawn (re-uploads when the player re-picks during spawn phase) - `WebGLFrameBuilder.syncPlayers` collects unique skin URLs on first sync and calls `view.initSkinAtlas(urls)` once; `clearCaches()` resets so seek/replay re-initializes - `territory.frag.glsl`: skin branch is mutually exclusive with patterns; bounds-checks UVs against `[0, 1]` so the image is a single stamp, not tiled; alpha-blends against the player palette color so transparent pixels and out-of-bounds tiles render as the regular player color **Hover highlight (global UX change, not skin-scoped)** - Existing hover highlight changed from "brighten toward white" to "saturation boost." Applies to all players regardless of skin/pattern/flat-color — looks better across the board. **UI** - `CosmeticButton` renders skins as a single `<img>` (object-contain) - `TerritoryPatternsModal` merges patterns + skins into one grid; single "default" tile clears both - Selecting a pattern clears the skin and vice versa (mutually exclusive) - `Store` pattern tab includes skin entries (purchasable, not-yet-owned) - `PatternInput` lobby button previews the active skin when one is set **Memory** - 0 skin players → ~4 bytes (placeholder) + ~40 KB fixed per-player tables - 1 skin player → ~5.6 MB GPU - 5 skin players → ~28 MB GPU - 10 skin players → ~56 MB GPU **Tests** - `tests/Privilege.test.ts`: 13 new cases covering `isSkinAllowed` (wildcard, exact-match, missing flare, missing skin, forged refs) and `isAllowed` integration (allowed/forbidden paths, short-circuit when invalid skin is paired with valid other cosmetics) ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] 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 - [ ] 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 |
||
|
|
ca565eaa1a |
Subscription upgrade/downgrade + tier management (#3927)
## Summary - Tier upgrade/downgrade in the Store. The Subscriptions tab now shows all tiers including the user's current one. Other tiers swap "Subscribe" → "Switch" when the user already has a sub, and clicking them calls the new `POST /subscriptions/@me/change-tier` endpoint with a direction-aware confirm (upgrade charges prorated diff now, downgrade gives account credit). - Owned-tier card renders a **Current Plan** badge in place of the purchase button. Resolution logic in `resolveCosmetics` now reads `userMeResponse.player.subscription.tier` (with flare fallback) and marks that tier as `owned`. - AccountModal's `<subscription-panel>` reworked into a proper two-column layout: - **Left**: tier name, `$X.XX/mo` price, description, daily Pu/Caps amounts. - **Right**: status badge (Active / Renews date / Cancels date), `[Manage] [Change Tier]` button row, `[Cancel]` centered underneath. When `cancelAtPeriodEnd === true`, the row collapses to a single `[Reactivate]` button (opens the Stripe portal). - New `<o-button size="xs">` variant (`py-2 px-3 text-xs`) for the compact panel buttons. - Store dollar-purchase price label now supports an optional suffix (`/mo` for subs only) via a `priceSuffix` prop plumbed through `CosmeticContainer` → `PurchaseButton`. - `Api.ts` gains `changeSubscriptionTier(tierName)` with the same 401-handling pattern as the existing subscription helpers. <img width="1114" height="728" alt="Screenshot 2026-05-14 at 7 09 20 PM" src="https://github.com/user-attachments/assets/688f83d5-4010-4580-9214-6885af8ec98e" /> <img width="1038" height="276" alt="Screenshot 2026-05-14 at 7 09 33 PM" src="https://github.com/user-attachments/assets/458197f5-a0d4-4c32-bc55-31e5679629b5" /> <img width="887" height="286" alt="Screenshot 2026-05-14 at 7 09 55 PM" src="https://github.com/user-attachments/assets/8149ed82-89cc-4bbe-83de-3614f886b331" /> ## Discord evan |
||
|
|
e0f73598d6 |
Add subscriptions: store tab, account panel, manage/cancel (#3918)
## Summary
- Add a **Subscriptions** tab to the Store. Each tier renders as a
`<cosmetic-button>` with description, daily Pu/Caps amounts, and a
Stripe checkout button driven by the existing `createCheckoutSession`
flow.
- Show the player's active subscription in the **Account modal** via a
new `<subscription-panel>` Lit component (status badge, period-end /
cancel-at-period-end, daily currency breakdown).
- **Manage** button opens the Stripe billing portal in a new tab (`POST
/subscriptions/@me/portal`).
- **Cancel** button (hidden once `cancelAtPeriodEnd === true`) calls
`POST /subscriptions/@me/cancel` after a `confirm()` prompt, then
invalidates the userMe cache and refetches.
- Block re-purchase: clicking Subscribe when the user already has a
`subscription:*` flare alerts "Already subscribed" before opening
checkout (upgrade/downgrade flows are out of scope for now).
- Schema additions:
- `CosmeticsSchema.subscriptions: Record<string, SubscriptionSchema>`
(optional) in `src/core/CosmeticSchemas.ts`.
- `UserMeResponse.player.subscription: { tier, status, currentPeriodEnd,
cancelAtPeriodEnd } | null` in `src/core/ApiSchemas.ts`.
- Translations: new `store.*` and `account_modal.sub_*` keys in
`resources/lang/en.json` (English only — Crowdin handles the rest).
-
<img width="942" height="313" alt="Screenshot 2026-05-14 at 1 13 05 PM"
src="https://github.com/user-attachments/assets/3d28df13-9e03-49f0-bee8-a25f9ad0c420"
/>
<img width="545" height="439" alt="Screenshot 2026-05-14 at 1 13 32 PM"
src="https://github.com/user-attachments/assets/b413b275-d6f2-40dc-9230-d68cd11fb07a"
/>
## Discord
evanpelle
|
||
|
|
616ba1c794 |
Add support to purchase cosmetics with in-game currency (#3648)
## Description: Caps & Plutonium can be used to purchase different cosmetics. * The cosmetic button can display pluto/caps/dollars * Create a "purchaseCosmetic" helper function that handles purchase logic ## 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 |
||
|
|
696e727a39 |
support for purchasing currency packs (#3629)
## Description: Adds a currency pack system to the store. Players can purchase packs of in-game currency (Plutonium and Caps) via Stripe checkout. What's new: * Pack schema (PackSchema) — new cosmetic type with currency (hard/soft), amount, and displayName * "Packs" tab in the Store — renders purchasable currency packs using existing CosmeticButton infrastructure * Stripe checkout flow — new createCurrencyPackCheckout API call and handlePackPurchase handler * Currency display in Account modal — shows Plutonium and Caps balances when logged in I* con components — <plutonium-icon> (animated green glow + rotate) and <cap-icon> with new SVG assets * Currency in UserMeResponse — player.currency.hard / player.currency.soft added to the API schema ## 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 |
||
|
|
d5a2cc0fca |
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 |