Commit Graph
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>
2026-07-15 15:54:01 -07:00
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>
2026-07-15 12:49:45 -07:00
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>
2026-07-13 19:55:40 -07:00
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>
2026-07-10 15:40:09 -07:00
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>
2026-07-03 12:41:39 -07:00
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>
2026-07-02 15:21:26 -07:00
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>
2026-07-02 14:21:01 -07:00
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>
2026-06-30 20:13:41 -07:00
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>
2026-06-29 21:53:33 -07:00
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>
2026-06-29 13:24:09 -07:00
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>
2026-06-29 13:13:48 -07:00
RyanandGitHub 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
2026-06-28 18:41:32 -07:00
RyanandGitHub 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
2026-06-27 19:09:05 -07:00
EvanandJosh Harris 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
2026-05-31 15:09:36 +01:00
EvanandGitHub 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
2026-05-27 13:00:07 -07:00
EvanandGitHub 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
2026-05-15 12:01:31 -07:00
EvanandGitHub 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
2026-05-14 13:47:16 -07:00
EvanandGitHub 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
2026-04-13 10:19:43 -07:00
EvanandGitHub 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
2026-04-10 15:07:47 -07:00
EvanandGitHub 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
2026-04-09 21:07:07 -07:00