mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 12:22:11 +00:00
64a6111fd492b8bd1810e8f52c88fdf3ac746e45
83 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b72956d0c0 |
Gate users without GPU-accelerated WebGL2 instead of running at ~1fps (#4324)
## Problem After the WebGL2 renderer migration, a small number of users (~a dozen of 100k DAU) report ~5fps. Root cause: they run WebGL **without GPU acceleration** (hardware acceleration disabled, blocklisted driver, or a locked-down machine), so they get a SwiftShader/software context. Software-rendered WebGL is hopeless for a real-time game — ~1fps locally. We are not supporting a canvas2d fallback. Instead: demand a GPU-accelerated context, and if we can't get one, **gate** the user with actionable instructions rather than letting the game crawl. ## What this does - **`initGL()`** ([initGL.ts](../blob/webgl-software-render-gate/src/client/render/gl/initGL.ts)) — demands `failIfMajorPerformanceCaveat: true` **and** inspects the unmasked renderer string. The flag alone isn't enough: when hardware acceleration is turned off in browser *settings* (vs. a blocklisted driver), Chrome still hands back a SwiftShader context, so we'd otherwise run at 1fps. Classifies the outcome as `ok` / `software` / `unsupported`. - **`GPURenderer`** throws `GLUnavailableError` on a non-accelerated context; the game-start `catch` shows the gate and removes the orphaned canvas. - **`<webgl-gate>`** Lit component renders a full-screen blocking gate with per-browser steps (Chrome / Edge / Firefox / Safari) for enabling hardware acceleration / WebGL. - **`gl_init` analytics event** fires every session (`status` + `renderer` for non-ok) via the existing Google Tag, so we can size the real affected % within a day. ## Notes / decisions - The gate copy is **intentionally inlined (not translated)** — it's a rarely-seen, browser-specific troubleshooting screen; 28 Crowdin keys would be poor cost/benefit, and a non-English user still has to navigate English browser menus. - `showGLGate` lazy-loads the component (`import()`), so the `render/gl` module that `Renderer.ts` imports doesn't statically pull a UI component into its graph. ## Update: fingerprint-capped contexts (#4357) A third failure class, integrated after the initial PR: `privacy.resistFingerprinting` (default-on in LibreWolf and Mullvad Browser, opt-in in Firefox) caps `MAX_TEXTURE_SIZE` at 2048 on an otherwise hardware-accelerated context. The renderer unconditionally allocates a 4096-wide palette texture, so the oversized `texImage2D` calls fail silently and the whole map renders **black** (#4357). - `initGL` now reads `MAX_TEXTURE_SIZE` after the software check and classifies the context as **`limited`** when it's below `getPaletteSize()` (4096 — the hard floor every game needs). - Unlike `software`/`unsupported`, **`limited` is a warning, not a hard block**: `initGL` still returns the context, the game starts normally, and the gate is shown with a "Continue anyway" button. `GPURenderer` exposes the capped renderer/size via `glLimited` (surfaced through `MapRenderer`), which `ClientGameRunner` uses to show the warning and log analytics. - The gate shows fingerprinting-specific instructions for `limited` (add the site to `privacy.resistFingerprinting.exemptedDomains` in `about:config`) instead of the hardware-acceleration steps. - `gl_init` reports `max_texture_size` alongside the renderer for this status, so we can size the RFP-affected population too. Fixes #4357 ## Test plan - [x] Unit tests for `initGL`'s `ok` / `software` / `unsupported` branching, incl. the "returns a context but renderer is software" case (`tests/client/initGL.test.ts`). - [x] lint / prettier / tsc clean. - [x] **Verified in real browsers (macOS).** All three gate states reproduced: - `software`: Chrome with `--use-gl=angle --use-angle=swiftshader` (confirmed "Software only" at `chrome://gpu`), and Chrome with hardware acceleration toggled off in settings — both show the hard gate instead of a 1fps game. - `unsupported`: Firefox with `webgl.disabled=true` shows the unsupported gate. - `limited`: Firefox with `privacy.resistFingerprinting=true` (MAX_TEXTURE_SIZE capped to 2048, same as LibreWolf's default) shows the dismissible warning; "Continue anyway" starts the game, and exempting the site via `privacy.resistFingerprinting.exemptedDomains` removes the warning. ## Acceptance criteria - Accelerated users: unchanged. - Software / no-accel users: see the enable-acceleration gate, not a 1fps game. - No-WebGL2 users: see the unsupported gate. - `gl_init` fires every session with status (+ renderer for non-ok). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
006f1690a5 |
Warship veterancy (#4433)
## Description: Warship veterancy! This is an idea inspired by the unit veterancy feature of games like C&C: Red Alert 2 in which unit eliminations increases the level of individual units. I've been trying to build this mechanic for months with different ideas, and I finally landed on this being one of the more balanced implementation. Warships can earn up to three levels, represented by the gold bar insignia in the bottom right of their warship sprite. <img width="622" height="202" alt="image" src="https://github.com/user-attachments/assets/a8c31a45-4ae9-41a9-b054-9c4a7f4ab1f1" /> A veterancy bar grants 20% health from the base amount, and a 20% increase in shell damage applied _after_ the random damage roll. For example, a level 3 warship will apply a 60% damage boost on top of the random shell damage value (something between 200-325. If the random value is 250, the final damage output will be `250 * 1.60 = 400`. There are three ways to achieve a veteran level: 1. **Eliminate another warship:** any time a warship neutralizes another warship, it immediately get's a veterancy increase. https://github.com/user-attachments/assets/6a9e0958-5171-4ca3-94f6-9c2300a12f8b 2. **Eliminate transport boats:** Destroying 10 transport boats will level a warship to the next veterancy bar. https://github.com/user-attachments/assets/619ce0c0-033c-4e0b-9c64-b41eabaa791b 3. **Steal trade ships:** If the warship captures 25 trade ships, it will earn a veterancy bar. ## 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: bijx |
||
|
|
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> |
||
|
|
dae129c6a3 |
replace leave lobby popup with custom popup (#4449)
## Description: old: <img width="1009" height="491" alt="image" src="https://github.com/user-attachments/assets/0b95877c-dac7-4025-bdfa-62ab6879d208" /> new: <img width="1017" height="561" alt="image" src="https://github.com/user-attachments/assets/cfb49b31-eb46-4d64-bd9e-3f25bb7cd0fb" /> ## 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 |
||
|
|
8ffb19d938 |
Discord (#4367)
Resolves #(issue number) ## Description: continuation of https://github.com/openfrontio/infra/pull/359 adds ability to put discord URL into a dedicated slot pc: <img width="1917" height="921" alt="image" src="https://github.com/user-attachments/assets/100a25d5-e998-4744-904e-df40b74ccd76" /> mobile: <img width="385" height="826" alt="image" src="https://github.com/user-attachments/assets/de904f83-c88f-41e7-9c98-81c2296ec9a2" /> ## 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> |
||
|
|
117fa43947 |
Fix nuke preview showing teammate SAMs as threats (#4342)
## Problem In the nuke trajectory preview, the SAM-intercept **"X"** marker was drawn over **teammates'** SAMs — implying their SAM would shoot down your missile. It shouldn't: like allies, a teammate's SAM never engages your nuke. The bug only affected teammates; allies already worked. ## Cause The preview built its threat set from `myPlayer.allies()` only — formal alliances — and never considered teammates. That diverged from the sim ([`SAMLauncherExecution.ts`](src/core/execution/SAMLauncherExecution.ts#L118-L134)), which skips any nuke whose owner it's `isFriendly()` with (**same team OR allied**). ## Fix `samThreatensNukePreview` now takes a teammate set and excludes teammates **unconditionally**. The subtlety: allies keep the existing *betrayal* exception — a strike close enough to break the alliance makes that ally's SAM engage at launch (`listNukeBreakAlliance`, the same function the sim uses). Teammates get **no** such exception, because a strike can break an alliance but never a team relationship. So even a player who is both a teammate *and* a betrayed ally is correctly left off the threat set. ## Notes - The sim has an "aftergame fun" exception where teammate SAMs *do* target teammate nukes once there's a winner. The preview only appears while aiming a buildable mid-game (no winner yet), so that case doesn't apply here. ## Tests Updated `samThreatensNukePreview` unit tests for the new signature and added coverage for: teammate excluded, and teammate stays excluded even when listed as betrayed. All 11 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bb5e7dc954 |
Apply perceptual curve to volume sliders (#4272)
## Problem Players reported having to turn the volume slider down to ~20% before noticing any change in loudness. The sliders fed their linear 0–1 position straight to Howler's `volume()`, which is linear amplitude gain. Human loudness perception is roughly logarithmic, so the top ~80% of the slider all sounds nearly identical — the classic linear-fader problem. ## Fix Square the slider position into a perceptual (audio-taper) gain inside `SoundManager`. The stored setting and the displayed `%` remain the intuitive linear slider position; only the gain handed to Howler is curved. | Slider | Old gain (linear) | New gain (x²) | |--------|-------------------|---------------| | 100% | 1.00 | 1.00 | | 90% | 0.90 | 0.81 | | 80% | 0.80 | 0.64 | | 50% | 0.50 | 0.25 | | 20% | 0.20 | 0.04 | Lowering the slider from 100→80 now produces an audible drop instead of nothing until ~20%. ## Notes - Quadratic (x²) was chosen as a balanced, conservative taper. Cubic (x³) would make the top-end drop-off even more immediate if preferred. - Existing saved settings are unaffected; the same slider position will simply sound slightly quieter, which is the intended correction. ## Tests Updated `SoundManager.test.ts` to assert the curved gain and added a dedicated test locking in the top-of-range behavior. All 18 tests pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f4db4a33c8 |
Send nukes as motion plans and render them smoothly per frame (#4255)
## Summary Follow-up to #4244's payload work: nukes were the last per-tick movers flooding the worker → main update stream. - **Core**: nuke trajectories are fully determined at launch (precomputed parabola), so `NukeExecution` now records a `GridPathPlan` when the nuke is built — same mechanism trade ships use — and the client derives the position each tick. Per-tick `UnitUpdate`s for nukes in flight are suppressed; only targetable flips and deletion (interception/detonation) still emit. This covers atom bombs, hydrogen bombs, and MIRV warheads (dozens of per-tick movers per MIRV separation). - The plan path replays a separate pathfinder rather than reusing the stored trajectory array: the curve's cached points don't advance exactly one index per tick, and the plan must match the movement pathfinder's exact per-tick tile sequence. - `startTick` accounts for MIRV warheads' staggered `waitTicks`. - **Render**: `UnitPass.drawMissiles` now lerps each nuke's instance position `lastPos→pos` by wall-clock progress through the current tick, so nukes glide along their arc at render framerate instead of jumping once per 100ms tick. Both endpoints are real simulated positions — the rendered nuke trails the sim by at most one tick and settles exactly on it when ticks stop. Plan-driven units sync `lastPos` on path-stall ticks so the lerp never replays a segment. Shells keep their existing two-instance trail; SAM missiles are unchanged. ## Test plan - New `tests/nukes/NukeMotionPlan.test.ts`: tick-exact alignment between the recorded plan and core nuke position over the whole flight (mirroring `GameView.advanceMotionPlannedUnits` math), `waitTicks` offset, and that no per-tick unit updates are emitted in flight except targetable flips and deletion. - Full suite passes (1452 + 65), tsc/eslint/prettier clean. - Verified in-game (headless Chromium, real WebGL): atom bomb arcs from silo to target with the client position driven by the plan, missile sprite renders intact while the smoothing rewrites the instance buffer every frame, detonation FX land at the target. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bca980f572 |
Shrink the per-tick worker → main update payload by ~90% (#4244)
Stacked on #4243 (the `perf:client` harness) — first step of fixing the every-100ms main-thread stutter: make the per-tick burst small before spreading what remains across frames. ## Problem The harness showed the main-thread burst was dominated by `structuredClone` of the `updates` object, and the clone was dominated by two kinds of per-tick churn that re-sent object payloads every tick: - `gold` / `troops` / `tilesOwned` change for nearly every alive player every tick → ~278 partial `PlayerUpdate` objects per tick (world/400 bots), ~508 on giantworldmap. - Attack troop counts tick down every tick → whole `outgoingAttacks`/`incomingAttacks` arrays re-cloned for every fighting player every tick. - `playerNameViewData` (an all-players record) was cloned every tick but only recomputed every 30 ticks. ## Change Three additions to the worker → main protocol (all transferable, zero-clone): 1. **`packedPlayerUpdates`** — `[smallID, tilesOwned, gold, troops]` float64 quads for players whose stats changed. These fields no longer appear in `PlayerUpdate` diffs (first emissions still carry the full snapshot). Gold is exact in a float64 (game values ≪ 2^53). 2. **`packedAttackUpdates`** — `[ownerSmallID, direction, index, troops]` quads. Attack arrays are only resent when membership/order/retreating changes — which is exactly the condition that keeps the patch indexes valid (a tick either resends an array or patches it, never both). 3. **`playerNameViewData` is now optional** — attached only on placement-rebuild ticks (spawn ticks, first ticks, every 30th, spawn end). The client keeps the last applied values; dead players' name placements freeze at death (matching the previous effective behavior). On the client, `GameView.populateFrame` now also rebuilds `names` / `relationMatrix` / `allianceClusters` only when their inputs changed that tick — field presence on a partial `PlayerUpdate` marks them dirty. (`playerStatus`, nuke telegraphs, and attack rings still recompute every tick; they're tick- or unit-dependent.) ## Results (perf:client, this machine; low-end devices ~5–20× slower) Default run (world, 400 bots, 1800 ticks): | stage | before | after | |---|---|---| | clone (serialize+deserialize) | 1.02ms | **0.09ms** | | GameView.update | 0.62ms | **0.29ms** | | WebGLFrameBuilder.update | 0.04ms | 0.04ms | | **TOTAL burst mean** | **1.67ms** | **0.42ms** | | TOTAL p99 / max | 3.47 / 10.3ms | **1.21 / 3.92ms** | giantworldmap/600t: 2.54 → 0.68ms mean. Player update objects: 278 → 6.5 per tick (world), 508 → 12 (giant). The remaining burst is mostly tile apply + per-tick derivations — the part that frame-spreading (next step) addresses. ## Verification - **Sim final hash unchanged** on all three reference configs (`5607618202213430`, `29309648281599524`, `39945089450032050`) — no simulation behavior change. - **View hash unchanged** on all three configs (`942106e9`, `a3aae227`, `cbaaf265`) — the rendered view state is provably identical tick-for-tick, including the name-freeze semantics. - New tests: `tests/PackedPlayerUpdates.test.ts` (drain + GameRunner cadence), packed-channel and freeze-at-death cases in `tests/client/view/GameView.test.ts`, `packAttackTroopDeltas` unit tests and updated diff contract in `tests/GameUpdateUtils.test.ts` / `tests/PlayerUpdateDiff.test.ts`. - `npm test` (1490 tests), `eslint`, `prettier`, `tsc --noEmit` all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
03b405eea7 |
Color nuke telegraph circles by launcher relation (self/ally/enemy)
The blast-radius warning circle was always red, so players couldn't tell who launched an incoming nuke. Now it's green for your own nukes, yellow for ally/teammate nukes, and red for everyone else's. Each telegraph carries a relation (0=self, 1=friendly, 2=enemy), classified from the per-tick relation matrix — the same friend/foe logic alt-view uses — and passed to the shader as a per-instance attribute. Replay/spectator mode (no local player) stays all red. Colors are tunable via the nukeTelegraph slice in render-settings.json. |
||
|
|
b85d1fc372 |
Fix alt-view coloring teammates as enemies in team games (#4247)
## Problem In team games, alternate view (space-hold) colored teammates' units red (enemy color) instead of yellow (ally color). Teammates' territory borders had the same problem. ## Root cause `buildRelationMatrix()` in `src/client/render/frame/derive/RelationMatrix.ts` already supports an optional `teams` map that marks same-team pairs as `RELATION_FRIENDLY`, but the call site in `GameView.populateFrame()` never passed it (the companion `buildTeamMap` helper was dead code). Only explicit alliances were marked friendly, so a teammate without a formal alliance read as neutral — and the alt-view unit palette maps neutral to the enemy color. ## Fix - `GameView` now tracks a `smallID → team` map, populated when each `PlayerView` is first created (team is a static field, so once per player is enough). - The map is passed through to `buildRelationMatrix()`, which feeds both the `AffiliationPalette` (unit colors) and `BorderComputePass` (border colors). ## Testing - New regression test in `tests/client/view/GameView.test.ts`: same-team players are `RELATION_FRIENDLY` in `frame.relationMatrix`, cross-team players stay neutral. - All 36 GameView tests pass; typecheck clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa4b490e68 |
Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240)
## Summary The WebGL renderer was adapted from an external extension and carried a lot of machinery this integration never uses (replay playback, its own input/event system, a GL radial menu). This PR is two mechanical cleanup passes with **no behavior change**: delete the dead code, then untangle the `GameView` naming collision. **78 files, +142 / −2,197.** ### Pass 1 — remove dead extension baggage - **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`; the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed), `applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and `GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes `types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`, `types/Game.ts` (imported only by the types barrel). - **FrameEvents**: trimmed from 14 fields to the 3 actually populated and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11 fed the extension's stats system and were never written or read here. - **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API methods on facade + renderer had zero callers — the game uses the DOM/d3 radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and drawn every frame for nothing. - **Facade event system**: `GameViewEventMap` defined 10 event types (`click`, `hover`, `scroll`, …) but only `contextrestored` was ever emitted — input actually flows through `InputHandler` → EventBus → controllers. Replaced the listener map with a single `onContextRestored` callback and deleted `Events.ts`. Also fixed the stale header comment claiming the facade handles user interaction. - **Unused API surface**: removed ~20 facade/renderer methods with zero callers (camera passthroughs like `panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`, `onFrame`/`afterRender`/fps tracking). Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks for a possible future camera unification) and the `timeFn` plumbing inside the FX passes (deeply embedded as defaults; only the dead renderer-level wrappers were removed). ### Pass 2 — untangle the three GameViews - `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`). Every importer was already aliasing it as `WebGLGameView` to dodge the collision with the simulation-mirror `GameView` in `client/view/`, so this removes aliasing rather than adding churn. `render/CLAUDE.md` updated. - Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO asked for this). All 51 importers now import from `src/client/view/` directly via a new 3-line barrel `view/index.ts`. ## Test plan - `tsc --noEmit` clean, `eslint` clean - Full test suite passes (1,385 + 65 server tests) - Manual verification via headless Chromium: started a singleplayer game and confirmed the renderer works end-to-end — terrain draws, spawn-phase overlay shows, territories fill with borders after spawning, player names/flags render, no renderer console errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
71af72606a |
Fix nuke trajectory preview missing SAM interception for would-be-betrayed allies (#4235)
## Summary Fixes #4226 (Release Blocker, V32 regression). The WebGL nuke trajectory preview built its SAM threat set by unconditionally excluding own + allied SAMs (`BuildPreviewController.updateNukeTrajectoryPreview`). But when the strike targets allied territory, the alliance breaks at launch — `NukeExecution.maybeBreakAlliances()` — so the betrayed ally's SAMs **do** engage the nuke. The preview therefore showed a fully white trajectory with no intercept X over an allied SAM, even though the bomb would be shot down (V31 previewed this correctly). ## Fix - Compute the would-be-betrayed player set with `listNukeBreakAlliance()` — the exact function the sim uses at launch, so preview and sim can't drift. - Keep an allied SAM in the threat set iff its owner is in that set (extracted as pure `samThreatensNukePreview()`). - Other (non-betrayed) allies' SAMs remain excluded, matching sim behavior where only alliances over the blast threshold break. Both missing artifacts in the issue (red post-intercept segment and X marker) come from `tSamIntercept` staying at 1.0 because no SAM was supplied, so this one change restores both. Cost note: this adds one `circleSearch` per throttled ghost update (50ms) when the player has allies — same order as the existing `wouldNukeBreakAlliance` call for the red warning circle. ## Testing - Unit tests for the new threat-set predicate (4 cases) in `tests/client/controllers/BuildPreviewController.test.ts` - `tsc --noEmit`, ESLint, Prettier clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2d747d0f8b |
Flash alliance icon when renewal prompt is active
When an alliance is within the renewal-prompt window, the alliance icon above the player's name now pulses, ramping from 2 Hz to 5 Hz as expiry approaches (same effect as the traitor flash). The flash window is driven by allianceExtensionPromptOffset() — the same Config value that triggers the "renew alliance" prompt in the actionable events display — so the two always stay in sync. The shader only knew the alliance fraction, not absolute time, so computePlayerStatus now also emits allianceRemainingTicks, packed into the free pd7.w slot of the player-data texture. |
||
|
|
3c0ff7a6f2 |
Fail open on clan tag ownership checks when API is unavailable
The clan-tag ownership check previously failed closed: when the API service was unreachable (e.g. during local development), the client dropped the tag with a "couldn't verify" error and the server's FailOpenPrivilegeChecker treated every unverifiable tag as reserved. This made clan tags unusable whenever the API was down. - Client: checkClanTagOwnership keeps the tag when the existence probe is inconclusive; the server still re-checks authoritatively. - Server: FailOpenPrivilegeChecker passes tags through instead of dropping non-member tags; decideClanTag now takes a non-nullable reserved set since the null case is gone. - Remove the now-unused username.tag_check_failed translation key. - Update Privilege and ClanApiQueries tests for fail-open behavior. Trade-off: if the reserved-tag list is unavailable in production, real clan tags can be impersonated until the first successful PrivilegeRefresher load; after that the last good checker is retained. |
||
|
|
9c2ac05506 |
clantag part 1 (#4066)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: adds a check to see if you're in a clan or not. if not, checks to see if the clan exists, if it does, warns the user, if it doesn't, lets them use it. ## 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: w.o.n |
||
|
|
b4a14f9b9d |
Move attack troop overlay to WebGL (#3996)
## Description: Replaces the DOM-based `AttackingTroopsOverlay` with `AttackingTroopsController`, rendering attack troop counts through `WorldTextPass` instead of a separate fixed-position DOM container. ## Summary - New `AttackingTroopsController` polls `attackClusteredPositions()` every 200ms and pushes labels to the WebGL view each frame, lerping cluster positions over 250ms for smooth front-line movement (replaces the old CSS `transform 0.25s` transition). - `WorldTextPass` gains `setAttackTroopLabels()` and renders them at a fixed on-screen size (zoom-independent) using `screenScale / zoom`. - World text now draws on top of `NamePass` so attack callouts aren't hidden behind centered player names. - Fragment shader adds a soft quadratic dark halo around every world-text label; extent uses the remaining SDF range after the hard outline so it fades smoothly to zero (no rectangular clipping). - Deletes `AttackingTroopsOverlay.ts`; existing unit tests repointed to the controller's exported `alignClusterOrder`. <img width="369" height="395" alt="Screenshot 2026-05-24 at 4 43 51 PM" src="https://github.com/user-attachments/assets/4dbffe20-77f9-4c0f-b956-ecf543538f8d" /> ## 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 |
||
|
|
545ad313e3 |
cleanup: drop unused disposer return from installSafariPinchZoomBlocker (#3992)
Follow-up to #3901 (cc @evanpelle). ## Description: In the review on #3901, evanpelle pointed out that the disposer returned by `installSafariPinchZoomBlocker` is never called at the call site in `Main.ts`, and asked whether there's any reason to return it. There isn't — the listeners live for the document's lifetime and the browser releases them on teardown — so this PR drops the disposer. ### Changes - `installSafariPinchZoomBlocker` now returns `void`. Removed the `return () => { ... }` block and the `@returns` JSDoc line. Added a sentence explaining why no disposer is needed. - Tests: dropped the disposer-removal test, switched the behavior tests to use fresh detached `<div>` elements (no document state leak across tests), and verified the default-target = `document` case with `vi.spyOn(document, 'addEventListener').mockImplementation(() => {})` so no real listener actually attaches to the shared jsdom document. Net diff: -23 lines (30 insertions, 53 deletions). ### What I tested - `npm test` — 1245 + 65 tests pass, including the 4 surviving tests for this helper - `npm run build-prod` — succeeds (tsc + vite) - `npx eslint` — clean - `npx prettier --check` on the touched files — clean ## 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: @vansszh |
||
|
|
a14cf0edc1 |
Clan Game History (#3988)
## Description: Adds <img width="1046" height="901" alt="image" src="https://github.com/user-attachments/assets/930b0d27-4707-4836-b068-620346e7e3a7" /> continuation of infra https://github.com/openfrontio/infra/pull/345 ## 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: w.o.n |
||
|
|
41ef675e98 |
Improve Notification Panel (#3913)
Resolves #3910 ## Description: - Split the events HUD into two components: a new **`<actionable-events>`** that owns alliance prompts (request / renew) and a slimmed-down **`<events-display>`** for everything else. - Reworked `<events-display>` into two visual tiers: dim/scrolling tier 2 on top (trade results, unit losses, donations, alliance status), prominent tier 1 anchored at the bottom (inbound nukes, naval invasion, attack requests, alliance broken, conquered player, chat). Tier 2 caps at the 4 newest entries; events expire after 8s. - Added a transient **+gold pip** above the gold pill in `<control-panel>`, animated with a small fade-in. Fires for trade ships, trains, donations, and conquest. Trade-ship and train arrivals are removed from the events scroll since they're surfaced here instead. - New `MessageType.NUKE_DETONATED` and a server-side emission in `NukeExecution.detonate` — once an inbound nuke lands or gets intercepted, the inbound warning vanishes and a "detonated" entry takes its place. - `displayMessage` gained optional `unitID` and `focusPlayerID` params so events can link to a unit or a player. Unit captures and destructions now navigate to the unit's last tile when clicked; donations navigate to the other player. - ActionableEvents card width matches `<events-display>`; cards persist until the user clicks Accept/Reject/Renew/Ignore or the server-side request timeout expires. - Removed the in-events category filter UI and the gold-amount banner — `<events-display>` is now a lightweight log that hides entirely when empty. <img width="570" height="444" alt="Screenshot 2026-05-21 at 1 42 30 PM" src="https://github.com/user-attachments/assets/f103efb3-0e11-4b72-a11b-91ff6896177c" /> <img width="430" height="296" alt="Screenshot 2026-05-21 at 1 41 34 PM" src="https://github.com/user-attachments/assets/ae58475a-b252-4aa6-9ce5-99dea7575ce3" /> ## 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 |
||
|
|
513057a62c |
WebGL: show alliance request+duration icon, show ally and team mate targets too, some optimization (#3971)
## Description: Show nuke icons during replay too (when there's no localPlayer). Show alliance request envelope icon, and duration in alliance icon (weren't calculated yet). Show ally and team mates' targets too (weren't calculated yet). Remove unnecessary allocations. Nukes loop allocated two new sets, transitive targets was a new set and now uses predicate with fallback to localPlayer.targets, localPlayer.allies and localPlayer.embargoes were both put in new set instead of using .includes directly. ## 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: tryout33 |
||
|
|
7863529b2c |
rename client/graphics → client/hud
The contents (Lit web components for in-game chat, build menu, leaderboard, attack displays, etc.) are HUD, not graphics — the actual graphics is in client/render/. |
||
|
|
4cd22a9b5c |
rename render/ files to UpperCamelCase to match client convention
The render/ tree was the only place in the client still using kebab-case filenames. Brings ~80 files in line with the rest of src/client/ (BuildPreviewController, TransformHandler, etc.). Directories kept as they were (name-pass/, fx-pass/, passes/, utils/, debug/) since the codebase already mixes those. Two collisions surfaced and got resolved: render/types/ is a directory, not a file, so its imports kept the lowercase form; and the sed pass incidentally normalized core/pathfinding imports, which had to be reverted since that file is actually lowercase on disk despite some imports having referenced it as ./Types under macOS case-insensitive resolution. |
||
|
|
a743a31897 |
delete dead canvas2D utilities, rename mountWebGLDebugRenderer → mountWebGLFrameLoop
ProgressBar and StructureDrawingUtils had no production callers — only their own test referenced ProgressBar, and StructureDrawingUtils was a canvas2D-era helper module that nothing imports anymore. mountWebGLDebugRenderer was named back when WebGL was a side-by-side debug overlay; it's the only renderer now, so the "Debug" prefix is misleading. Also dropped the `\` keybind that hid the GL canvas — with no other renderer, hiding it just blanks the game. |
||
|
|
7b1557b886 |
controllers push to the WebGL view directly, drop ClientGameRunner relays
BuildPreviewController and WarshipSelectionController now take the WebGL view in their constructor and call view.updateGhostPreview / view.setSelectedUnits themselves instead of emitting bus events that ClientGameRunner forwarded. Splits the old mountWebGLDebugRenderer in two — createWebGLView builds the view up front so the renderer can wire controllers to it, mountWebGLDebugRenderer does the per-frame plumbing after the transformHandler exists. GhostPreviewUpdatedEvent had no remaining consumers and is removed. |
||
|
|
a708a8c984 |
rename UILayer/StructureIconsLayer to controllers, move to src/client/controllers/
UILayer → WarshipSelectionController and StructureIconsLayer → BuildPreviewController. These are the two real Controller implementations (state + click handling, no rendering) — the new names + location reflect what they actually do now that all rendering lives in WebGL passes. |
||
|
|
923cba8c2d |
move multi-unit warship selection box to WebGL SelectionBoxPass
SelectionBoxPass now stores an array of selections and renders one quad per entry. GPURenderer gains setSelectedUnits(ids) — the single-unit setSelectedUnit becomes a wrapper. Position + color are rebuilt each frame from lastUnits; dead unit IDs get pruned in place. ClientGameRunner's UnitSelectionEvent listener forwards both single and multi to view.setSelectedUnits — no more single/multi split. UILayer drops everything canvas2D-related: the offscreen canvas + context, theme, selectionAnimTime, multiSelectionBoxCenters, SELECTION_BOX_SIZE, drawSelectionBoxMulti, paintSelectionBoxAt, clearSelectionBox, paintCell, clearCell, and renderLayer / redraw / shouldTransform. tick() now only prunes destroyed warships from the selection list; the layer is purely state + click handling. ~120 LOC gone. Tests: UILayer.test.ts updated — drops the canvas/redraw asserts, adds a multi-selection state assertion. |
||
|
|
ede0fb7668 |
move single-unit warship selection box to WebGL SelectionBoxPass
UnitSelectionEvent now forwards to view.setSelectedUnit(unit.id()) in mountWebGLDebugRenderer; the renderer's SelectionBoxPass draws the animated stippled outline on the GPU. UILayer still tracks selectedUnit for game-logic readers (the click handlers) but no longer paints to canvas2D for it. Drops drawSelectionBox + lastSelectionBoxCenter (~50 LOC) plus the per-tick single-unit redraw in tick(). Multi-selection stays on canvas2D — SelectionBoxPass is single-unit only. Test update: replaces the now-dead drawSelectionBox spy with a selectedUnit state assertion + a deselect case. |
||
|
|
45246f2085 |
make computePlayerStatus live-aware so status icons render
The replay-path computePlayerStatus left alliance/target/embargo/ nukeTargetsMe at false, which meant the WebGL NamePass had no data for those status icons after we switched names off canvas2D — they just stopped appearing. Add an opts param taking localPlayerID + tileState. When localPlayerID is set, fill the relative flags by checking the local player's allies/targets/embargoes against each other player's smallID; embargo is bilateral (either side). nukeTargetsMe walks active nukes and checks their targetTile's owner via the tileState buffer. Plumb localPlayerID = myPlayer?.smallID() and tileState from populateFrame so the live path uses the new mode. Emit an entry when only a relative flag is true (previously could be dropped if no base flag was set). allianceReq and allianceFraction stay deferred (need local PlayerID string for outgoing requests and current tick for fraction). 18 new tests covering both modes — replay (relative flags forced false), and live (alliance one-way, target one-way, embargo bilateral, self-flags suppressed, nukeTargetsMe with/without tileState, relative-flag-alone emits, localPlayerID=0 falls back to replay, allianceReq/allianceFraction stay deferred). |
||
|
|
8955be7667 |
fix: store embargoes as smallID numbers (drop string[] wart)
PlayerState.embargoes was string[] of stringified smallIDs — the renderer parsed each entry with parseInt() to use as an array index. Flagged in the integration handoff as something that should be number[]. Switch to number[] end-to-end: renderer type, relation-matrix derive (no parseInt), PlayerView.setEmbargoSmallIDs / hasEmbargoAgainst (numeric Array.includes, no String() temporaries), and GameView's embargo translation pass. Also updates the PlayerView test that pinned the old format. |
||
|
|
5b663fae14 |
refactor: share renderer state shapes between game and WebGL renderer
PlayerView/UnitView now wrap renderer-shaped state objects (PlayerState, PlayerStatic, UnitState) directly instead of holding engine wire types. GameView owns a long-lived FrameData object kept in sync each tick: players/units/tiles/trail/railroad are mutated in place; derived buffers (playerStatus, relationMatrix, allianceClusters, nukeTelegraphs, attackRings) and events are recomputed in a final populateFrame() pass. The renderer reads gameView.frameData() and the same byte-identical state objects PlayerView/UnitView wrap. WebGLFrameBuilder shrinks from ~270 to ~70 LOC: palette management + a single uploadFrameData() call, no per-frame UnitState allocation on the hot path. Wiring: maxPlayers=1024 on RendererConfig (pre-sizes NamePass/palette/ relation matrix textures); NamePass disabled so HTML NameLayer remains the only on-screen player names. Also: 39 new tests covering PlayerView/GameView/FrameData behavior; replace .data field access in three layer call sites with accessor methods (betrayals(), type(), getTraitorRemainingTicks()). |
||
|
|
53cf2d43f8 | migrate away from canvas | ||
|
|
9e39a7f5a1 |
fix(client): block Safari page-level pinch-zoom (#3901)
iOS Safari has ignored the `user-scalable=no` viewport hint since iOS 10, so two-finger pinch still zooms the whole page and can softlock the in-game HUD. Intercept WebKit's non-standard `gesturestart`, `gesturechange` and `gestureend` events at `document` and call `preventDefault()` so the page stays put. The game's own pinch-to-zoom on the map canvas is driven by pointer events (InputHandler) and is unaffected; browsers that do not fire GestureEvent treat the listeners as a no-op. Resolves #2330 If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: Describe the PR. ## 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 - [ ] 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: DISCORD_USERNAME |
||
|
|
275fd0dccc |
refactor: collapse per-env Configs into ClientEnv + ServerEnv (#3906)
## Description: This is a refactor to simplify config handling. Replaces the per-environment DevConfig/PreprodConfig/ProdConfig class hierarchy with two static classes: ClientEnv (browser main thread, reads from window.BOOTSTRAP_CONFIG) and ServerEnv (Node server, reads from process.env). The four config classes are deleted, the abstract DefaultServerConfig is gone, and DefaultConfig is renamed to Config. The values that flow server → client (gameEnv, numWorkers, turnstileSiteKey, jwtAudience, instanceId) used to be baked into the hardcoded per-env classes. They're now real env vars on the server, embedded into a single window.BOOTSTRAP_CONFIG object in index.html at request time (alongside the existing gitCommit/assetManifest/cdnBase globals, which moved into the same object), and read back by ClientEnv on the client. The dev defaults previously hidden inside DevServerConfig are now explicit in start:server-dev (NUM_WORKERS=2, TURNSTILE_SITE_KEY=1x..., JWT_AUDIENCE=localhost, etc.) and in vite.config.ts's html plugin inject.data. Production deploys plumb NUM_WORKERS and TURNSTILE_SITE_KEY through deploy.yml (GitHub vars) into the remote env file; JWT_AUDIENCE is derived from DOMAIN in deploy.sh. The dynamic /api/instance endpoint is gone — INSTANCE_ID rides along in BOOTSTRAP_CONFIG now. ServerEnv is the only thing server code touches; ClientEnv is browser-only. The two classes have intentional overlap (env, numWorkers, jwtIssuer, gameCreationRate, workerIndex, etc.) since they derive identical logic from different sources — there's a TODO in each to consolidate via a shared helper later. The game-logic Config no longer stores a ServerConfig/ClientEnv reference and its serverConfig() getter is gone; the one caller (MultiTabModal) now reads ClientEnv.env() directly. Worker init no longer carries server-config values since nothing in the worker actually reads them. ## 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 |
||
|
|
4d0324c5cc | run formatter | ||
|
|
e960b5130a | run formatter | ||
|
|
005e1b6044 |
clan stats breakdown (#3869)
## Description: improvements to clan ui. <img width="788" height="290" alt="image" src="https://github.com/user-attachments/assets/736ca147-bff4-44d8-8180-7b80a85556fe" /> added "expand all" and new collapsible sections. <img width="787" height="550" alt="image" src="https://github.com/user-attachments/assets/deb2f813-854b-46a9-a767-52c4f749f30f" /> which changes to collapse all when expanded also adds more info about team (d,t,q,2,3,4,5,6,7 team) ## 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: w.o.n |
||
|
|
879d502eb7 | Merge branch 'v31' | ||
|
|
df84ee023e |
Refactor & standardize modal tabs (#3864)
## Description: Refactors tab handling out of the individual modal components and into the base o-modal component. Tabs are now declared by passing tabs, activeTab, and onTabChange props, and a new named header slot pins consumer-supplied content above the tabs. This standardizes the modal tab look. <img width="1089" height="290" alt="Screenshot 2026-05-06 at 12 17 33 PM" src="https://github.com/user-attachments/assets/08d5a039-0aef-4aa7-b972-1e43b8723685" /> ## 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 |
||
|
|
eca5794ebb |
Chore(deps): Update and remove dependencies (#3819)
## Description:
Only mentioning removals/major updates/notable changes below, not all
minor upgrades.
### Removed:
- "@aws-sdk/client-s3": not used anywhere (was used in Archive.ts
previously)
- chai, "@types/chai", sinon-chai: not used anywhere, probably leftover.
Vitest uses a bundled version of Chai for its expect asserations under
the hood too.
- protobufjs, "@types/google-protobuf": not used anywhere, probably left
from evan's experiment with it? Removed from vite.config.ts too.
- "@types/jquery": not used anywhere, probably leftover
- sinon, "@types/sinon": not used anywhere just like chai, probably
leftover. And Vitest provides us with the same functionality.
- "@types/systeminformation": dependency systeminformation was removed
last year, this is an unneeded, deprecated and unmaintained remainder.
- vite-tsconfig-paths: removed, and removed the import and usage in
vite.config.ts and replaced it by adding `tsconfigPaths: true` to the
`resolve` block. Because of this message displayed on running the tests:
"The plugin "vite-tsconfig-paths" is detected. Vite now supports
tsconfig paths resolution natively via the resolve.tsconfigPaths option.
You can remove the plugin and set resolve.tsconfigPaths: true in your
Vite config instead."
- vite-plugin-static-copy: removed, we don't use it anymore (was used in
our vite.config.ts once,, probably before Vite natively supported
copying static assets via its publicDir configuration)
### Updated:
- color.js: v0.5 > v0.6, no breaking change affecting us
- cross-env: v7 > v10. It's a publicly archived repo since Nov 2025. But
before that he got it up-to-date from June 2025, porting to TS, dropping
old Node versions, dependencies etc. Seems still good to use for some
amount of time to come.
- dotenv: v16 > v17, now logs an informational message by default when
it loads an environment file. Can be disabled by using
dotenv.config({quite: true}) if needed.
- ejs: v3 > v5: security patches mostly. Vite still uses v3 btw.
- eslint: v9 > v10. Newly enabled rules by default:
'no-unassigned-vars', 'no-useless-assignment' and
'preserve-caught-error'. Mostly faster and minimum support moved to
higher node versions, which shouldn't be a problem.
- "@eslint/compat": v1 > v2. Minimum supported Node versions, which
should not be a problem.
- intl-messageformat: v10 > v11 no breaking changes that affect us
- jdom: v27 > v29. Faster. Most notably minimum support moved to higher
node v22 version, which should not be a problem. Also, see types/node,
kind of expecting v24 to be installed now.
- nanoid: from v3 to v5, no breaking changes that affect us
- "@opentelemetry/sdk-logs": now that addLogRecordProcessor is removed,
changed Logger.ts to pass an (empty) provider array directly to the
LoggerProvider constructor. Follows the changes in
https://github.com/open-telemetry/opentelemetry-js/pull/5588
- "@tailwindcss/vite": supports vite v8 from 4.2.2, and a fix for it in
4.2.4
- tailwindcss: supports vite v8 from 4.2.2
-- in 4.1.15 (we were already above this version) break-words was
deprecated in favor of wrap-break-word. But break-words, which we use in
15 places, will still work as expected
(https://github.com/tailwindlabs/tailwindcss/pull/19157). Same goes for
also deprecated "order-none".
- "@types/node": from v22 to v24, assuming most now use node 24
- vite v7 > v8:
-- is now on 8.0.10 so first bugs are out of it, while v8 itself also
fixed a big number of bugs.
-- in vite.config.ts, fixed Ts error/compilation issue by changing the
manualChunks option in build.rollupOptions.output to use the function
syntax, which is required by the updated types instead of the object
syntax.
- zod: no changes that affect us
### Prettier:
Updated only because of (new because of update?) Prettier errors for
files untouched in this PR originally:
- PathFinder.Parabola.ts
- WorkerMessages.ts
- ClanModal.handlers.test.ts
- ClanModal.rendering.test.ts
- CONTRIBUTING.md
- README.md
### ESLint:
Fixes needed to silence errors coming from newly enabled recommended
rules 'no-useless-assignment' and 'preserve-caught-error':
For 'no-useless-assignment' (default assignment never used because of
unreachable code or they are guaranteed to get a value, so they can be
undefinedat the start. Exception was AttackExecution, so made the
default value of 0 the default case in the switch statement):
- ClientGameRunner
- GameModeSelector
- NameBoxCalculator
- StructureDrawingUtils
- TerritoryLayer
- Diagnostics
- GameRunner
- ColorAllocator
- DefaultConfig
- AttackExecution
- AiAttackBehavior
- Worker.worker
- GamePreviewBuilder
For 'preserve-caught-error', disabled the rule here because the possible
fix `{cause: error}` was introduced in ES2022 while we're still on
target ES2020 currently:
- GameServer
- Privilege
_Error: The value assigned to 'gameMap' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'timeDisplay' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'scalingFactor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'radius' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'teamColor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'gl' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'power' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'tickExecutionDuration' is not used in
subsequent statements. (no-useless-assignment)
Error: The value assigned to 'selectedIndex' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'mag' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'speed' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'matchesCriteria' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'shouldContinue' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'description' is not used in subsequent
statements. (no-useless-assignment)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)_
All tests pass. TypeScript and ESLint errors resolved.
## 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:
tryout33
---------
Co-authored-by: Copilot <copilot@github.com>
|
||
|
|
94bab78d24 |
Fix off by one error (#3827)
## Description: Currently it is impossible to search for 2 letter clan tags (UN, FR, EU), this is because of an off by one error present in the API ## 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: Babyboucher |
||
|
|
b8a544a7d5 |
Fix off by one error (#3827)
## Description: Currently it is impossible to search for 2 letter clan tags (UN, FR, EU), this is because of an off by one error present in the API ## 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: Babyboucher |
||
|
|
08b9fd96e6 |
simplify attack overlay to reduce visual clutter (#3848)
## Description: Simplifies the attacking-troops overlay: removes the soldier icon and strength bar, dropping each label down to just the troop number in cyan (outgoing) or red (incoming) with a soft dark text-shadow halo and no background fill so territory borders show through cleanly. Also splits the label into outer (transitioned position) and inner (instant scale) divs so zoom changes no longer get smeared by the 0.25s cluster-move transition, retunes the zoom→size curve, and skips incoming labels from bot tribes to cut clutter. <img width="374" height="307" alt="Screenshot 2026-05-04 at 5 53 17 PM" src="https://github.com/user-attachments/assets/a7044221-06cc-4027-b19a-6ff4ca8f542a" /> ## 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 |
||
|
|
df05d21fc2 |
Clan System Part 2 - UI (#3625)
## Description: Continuation from #3276 Adds the complete client-side clan UI as a Lit web component (`<clan-modal>`), a typed API client with Zod-validated responses, shared response schemas, and a reusable `<confirm-dialog>` component. ### New: `ClanModal.ts` | View | What it does | |------|-------------| | **My Clans** | Lists joined clans + pending join requests (built from `/users/@me`, no extra fetches) | | **Browse** | Search by tag (min 3 chars), paginated results, configurable per-page (10/25/50) | | **Clan Detail** | Stats, paginated + searchable member list, role badges, join/leave/request actions | | **Manage** | Edit name (max 35 chars) + description, toggle open/invite-only, disband | | **Transfer** | Leadership transfer with member selector + confirmation | | **Requests** | Approve/deny join requests (leader/officer) | | **Bans** | View and unban (leader/officer) | | **My Requests** | View and withdraw outgoing requests | ### New: `ConfirmDialog.ts` Reusable `<confirm-dialog>` Lit component — replaces native `confirm()`/`prompt()` which are blocked or broken on mobile and CrazyGames iframes. Supports danger/warning variants and an optional textarea (used for ban reasons). Fires `confirm`/`cancel` events. ### New: `ClanApi.ts` Typed API client covering all clan endpoints. Every response is Zod-validated. Auth header is always last in the spread (can't be overridden by callers). Unknown server error messages always fall back to a generic client-side string — never displayed verbatim. ### New: `ClanApiSchemas.ts` (in `src/core/`) Shared Zod schemas for clan API responses with max-length constraints on `name` (35) and `description` (200). Lives in `core/` so it can be consumed by both client code and the leaderboard table. ### Modified: `ApiSchemas.ts` - Added `clans` and `clanRequests` arrays to `UserMeResponseSchema` - Moved clan leaderboard schemas out to `ClanApiSchemas.ts` - Renamed `LeaderboardClanTagSchema` → `RequiredClanTagSchema` ### Modified: `Api.ts` - Added `invalidateUserMe()` to bust the cached `/users/me` response after mutations - Removed `fetchClanLeaderboard` (moved to `ClanApi.ts`) ### Tests - `ClanModal.test.ts` — rendering, view navigation, user actions - `ClanApiQueries.test.ts` — fetch functions, error handling, pagination - `ClanApiMutations.test.ts` — join, leave, kick, ban, promote, transfer, etc. - `ClanApiBans.test.ts` — ban/unban calls and error paths - `ClanApiSchemas.test.ts` — Zod schema validation edge cases - `LeaderboardModal.test.ts` — updated imports ## Notable design decisions - **Not-logged-in state** — shows "Sign in to join clans" instead of false "no clans" empty state - **Rate limit feedback** — reads `Retry-After` header and surfaces wait time to the user ## 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: w.o.n --------- Co-authored-by: evanpelle <evanpelle@gmail.com> |
||
|
|
0c0f9c2a81 |
Update attack labels (#3784)
## Description: The motivation behind this PR is to standardize colors & icons for incoming and outgoing attacks. Outgoing attacks are always aquarious and incoming are red. This also makes it much easier to see which attacks are incoming vs outgoing at a glance, as previously the color changed depending on attack effictiveness. Instead, show a small bar on the left side that displays attack effectiveness. <img width="498" height="456" alt="Screenshot 2026-04-27 at 12 58 53 PM" src="https://github.com/user-attachments/assets/ea6928b3-5dfa-47fa-84d2-63e1e81ef6a4" /> Updates the in-game attack labels to match AttacksDisplay: a single soldier icon recolored via CSS filters, aquarius for outgoing and red-400 for incoming. Color is now purely directional — the previous attacker-vs-defender comparison (and the troopAttackColor / troopDefenceColor helpers that drove it) is gone, along with the defenderTroops plumbing. Also adds zoom-aware sizing via a new computeLabelScale(zoom) (full screen size when zoomed in, linear shrink with a floor so labels never disappear), bumps font/padding/snap-jump threshold for readability, and moves immutable per-label DOM writes (icon src/filter, color) into element creation so the per-tick path only updates the troop count. Also fixes a bug where the labels kept swapping when 2 clusters where similar size ## 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 |
||
|
|
c3d7d0373e |
Improve ingame moderation for admins (#3678)
## Description: Players with the `admin` flare can now kick players from any game (including public lobbies), not just the lobby creator in private lobbies. ## 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: w.o.n |
||
|
|
76f8441b45 |
feat: add warning news type and Firefox performance notice (#3680)
## Description: Adds a new `warning` news type to the news banner system and uses it to display a Firefox performance notice. Changes: - Added `warning` type with red styling to `NewsBox.ts` - Added `news_box.warning` key (`"WARNING"`) to `en.json` - Added Firefox performance notice to `resources/news.json` using the new `warning` type - Added `news_box.*` dynamic key pattern to `TranslationSystem.test.ts` to fix unused key detection ## 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 ## UI change: <img width="2101" height="1770" alt="CleanShot 2026-04-16 at 15 04 35@2x" src="https://github.com/user-attachments/assets/7a8b9290-4216-4799-b271-606afd9b8723" /> ## Please put your Discord username so you can be contacted if a bug or regression is found: fghjk_60845 |
||
|
|
1ebac8e854 |
Move brand images to proprietary/ and support multi-dir asset pipeline (#3662)
## Description: * Move proprietary brand images (logos, favicon) from resources/images/ to proprietary/images/ to separate open-source assets from proprietary ones * Extend the asset pipeline (PublicAssetManifest, vite.config.ts) to support multiple source directories (resources/ + proprietary/), so buildAssetUrl resolves assets from either location transparently * In dev, serve proprietary/ as a fallback middleware (registered after Vite's publicDir handler) so resources/ takes precedence when files exist in both. The idea is we could have placeholder assets placeholders that can be used by forks, and only the production build uses proprietary assets. ## 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 |
||
|
|
55e8a4edb7 |
feat: add NewsBox component and integrate news items into PlayPage (#3545)
Resolves #2998 ## Description: Adds a news box to the lobby homepage that advertises upcoming clan tournaments, weekly tournaments, and new player tutorials. The component sits above the username input and cycles through items automatically. <img width="1138" height="591" alt="screenshot-2026-03-31_00-48-33" src="https://github.com/user-attachments/assets/4b79287d-6aca-4c81-9bfe-36aad043f381" /> <img width="1107" height="595" alt="screenshot-2026-03-31_00-48-24" src="https://github.com/user-attachments/assets/598e6b8b-e0f2-4864-a5fb-a00c0cc98f37" /> <img width="1367" height="599" alt="screenshot-2026-03-31_00-48-04" src="https://github.com/user-attachments/assets/14f74e70-9dc0-4d67-af6e-c4708e539490" /> ## 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: deathllotus --------- Co-authored-by: Evan <evanpelle@gmail.com> |
||
|
|
18da7134c8 |
Implement FX sound effects (#3394)
## Description: Adds sound effects for approved events from the [sound asset pack](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing). 15 new sound effects triggered from `FxLayer`, `EventsDisplay`, and `RadialMenu`. Sounds play even when visual FX are off, so disabling explosions doesn't kill audio. Unapproved sounds are included as assets but not wired up yet. ### SoundManager architecture Reworked `SoundManager` per [maintainer feedback](https://github.com/openfrontio/OpenFrontIO/issues/1893#issuecomment-4184649434) and [follow-up review](https://github.com/openfrontio/OpenFrontIO/pull/3394): - No more singleton. `SoundManager` is instantiated in `createClientGame()` with `EventBus` and `UserSettings` - Layers emit events (`PlaySoundEffectEvent`, `SetBackgroundMusicVolumeEvent`, `SetSoundEffectsVolumeEvent`) via EventBus instead of holding a `SoundManager` reference - `SoundManager` subscribes to these events in its constructor - `SoundEffect` is a type union (not an enum), per project convention - All sound configuration (type, URL mapping, events) lives in `Sounds.ts` - Sound effects are lazy-loaded on first play - Channel limit of 8 concurrent sounds. New sounds always play; when at the limit, the oldest active sound gets stopped - `SoundManager` bootstraps volume from `UserSettings` in its constructor - All Howler calls are wrapped in try/catch with error logging, so sound failures never crash the game - `dispose()` method unsubscribes from EventBus and unloads all Howl instances on game shutdown - Sound code stays entirely in `src/client/`, nothing in `core/` touches it ## Sound approval status (per [spreadsheet](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing)) ### Approved, wired up in this PR | Event | Sound file | Trigger location | |-------|-----------|-----------------| | Message sent/received | `message.mp3` | EventsDisplay | | Menu open/select | `click.mp3` | RadialMenu | | Atom bomb launch | `atom-launch.mp3` | FxLayer (unit created) | | Atom bomb / MIRV hit | `atom-hit.mp3` | FxLayer (reached target) | | Hydrogen launch | `hydrogen-launch.mp3` | FxLayer (unit created) | | Hydrogen hit | `hydrogen-hit.mp3` | FxLayer (reached target) | | MIRV launch | `mirv-launch.mp3` | FxLayer (unit created) | | Alliance suggested | `alliance-suggested.mp3` | EventsDisplay | | Alliance broken | `alliance-broken.mp3` | EventsDisplay | | Port built | `build-port.mp3` | FxLayer (construction complete) | | City built | `build-city.mp3` | FxLayer (construction complete) | | Defense post built | `build-defense-post.mp3` | FxLayer (construction complete) | | Warship built | `build-warship.mp3` | FxLayer (unit created) | | SAM built | `sam-built.mp3` | FxLayer (construction complete) | ### Waiting for approval, sound files included but NOT wired up | Event | Sound file | Notes | |-------|-----------|-------| | Missile Silo built | `silo-built.mp3` | Waiting for Approval | | SAM shoot | `sam-shoot.mp3` | Waiting for Approval | | SAM hit | - | Waiting for Approval, no sound file assigned | | Warship sunk | - | Waiting for Approval, no sound file assigned | | Warship shoot | - | Waiting for Approval, no sound file assigned | ### Not done, no sound files exist yet | Event | Notes | |-------|-------| | Looted player | "Not sure if needed" | | Invaded | - | | Ship invasion incoming | - | | Ship sent | - | | Menu theme song | - | | Ambience | "Not sure if needed" | ## Test plan - [x] Start a private game and launch atom/hydrogen/MIRV nukes, verify launch and detonation sounds - [x] Build structures (city, port, defense post, SAM), verify build completion sounds - [x] Build a warship, verify warship built sound - [x] Receive an alliance request, verify alliance suggested sound - [x] Break an alliance, verify alliance broken sound - [ ] Receive a chat message, verify message sound - [x] Open the radial menu and click items, verify click sound - [x] Disable visual FX in settings, verify sounds still play - [x] Adjust SFX volume slider, verify it affects all new sounds - [x] Verify no audio issues with rapid/overlapping events - [x] Verify SoundManager responds to EventBus events and unsubscribes cleanly on dispose - [x] Verify SoundManager swallows Howler errors without crashing the game - [x] Verify channel limit of 8, oldest sound stopped when at cap ## Checklist - [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 Resolves #1893 ## Please put your Discord username so you can be contacted if a bug or regression is found: cool_clarky |