Merge branch 'v30'

This commit is contained in:
evanpelle
2026-04-16 19:40:05 -07:00
10 changed files with 197 additions and 172 deletions
+5 -2
View File
@@ -26,6 +26,11 @@
padding-left: env(safe-area-inset-left);
}
/* Prevent Playwire bottom rail from affecting flex layout */
[id^="pw-oop-bottom_rail"] {
position: fixed !important;
}
/* Ensure full viewport height on iOS */
html,
body {
@@ -261,9 +266,7 @@
></ranked-modal>
</main-layout>
<!-- Ad above footer -->
<div class="[.in-game_&]:hidden mt-auto flex flex-col shrink-0">
<home-footer-ad></home-footer-ad>
<page-footer></page-footer>
</div>
+1 -1
View File
@@ -150,7 +150,7 @@ export class FlagInputModal extends BaseModal {
</div>
<div class="flex justify-center py-3 shrink-0">
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wider rounded-lg bg-blue-600 hover:bg-blue-700 text-white cursor-pointer transition-colors"
class="no-crazygames px-4 py-2 text-sm font-bold uppercase tracking-wider rounded-lg bg-blue-600 hover:bg-blue-700 text-white cursor-pointer transition-colors"
@click=${() => {
this.close();
window.showPage?.("page-item-store");
+57 -107
View File
@@ -1,11 +1,6 @@
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
export const FOOTER_AD_MIN_HEIGHT = 880;
const FOOTER_AD_TYPE = "standard_iab_head2";
const FOOTER_AD_CONTAINER_ID = "home-footer-ad-container";
// ─── Gutter Ads ──────────────────────────────────────────────────────────────
@customElement("homepage-promos")
@@ -13,12 +8,6 @@ export class HomepagePromos extends LitElement {
@state() private isVisible: boolean = false;
@state() private adLoaded: boolean = false;
private cornerAdLoaded: boolean = false;
@state() private hasFooterAd: boolean = false;
private onResize = () => {
const isDesktop = window.innerWidth >= 640;
this.hasFooterAd = isDesktop && window.innerHeight >= FOOTER_AD_MIN_HEIGHT;
};
private onUserMeResponse = () => {
if (window.adsEnabled) {
@@ -30,6 +19,16 @@ export class HomepagePromos extends LitElement {
}
};
private onJoinLobby = () => {
this.loadBottomRail();
};
private onLeaveLobby = () => {
this.destroyBottomRail();
};
private bottomRailActive: boolean = false;
private leftAdType: string = "standard_iab_left2";
private rightAdType: string = "standard_iab_rght1";
private leftContainerId: string = "gutter-ad-container-left";
@@ -39,19 +38,18 @@ export class HomepagePromos extends LitElement {
return this;
}
static styles = css``;
connectedCallback() {
super.connectedCallback();
this.onResize();
window.addEventListener("resize", this.onResize);
document.addEventListener("userMeResponse", this.onUserMeResponse);
document.addEventListener("join-lobby", this.onJoinLobby);
document.addEventListener("leave-lobby", this.onLeaveLobby);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this.onResize);
document.removeEventListener("userMeResponse", this.onUserMeResponse);
document.removeEventListener("join-lobby", this.onJoinLobby);
document.removeEventListener("leave-lobby", this.onLeaveLobby);
}
public show(): void {
@@ -63,8 +61,10 @@ export class HomepagePromos extends LitElement {
}
public close(): void {
this.isVisible = false;
this.adLoaded = false;
try {
// Keep corner video ad alive.
// Only destroy gutter ads; bottom_rail persists into spawn phase.
window.ramp.destroyUnits(this.leftAdType);
window.ramp.destroyUnits(this.rightAdType);
console.log("successfully destroyed gutter ads");
@@ -73,6 +73,43 @@ export class HomepagePromos extends LitElement {
}
}
public loadBottomRail(): void {
if (!window.adsEnabled) return;
if (this.bottomRailActive) return;
if (!window.ramp) {
console.warn("Playwire RAMP not available for bottom_rail ad");
return;
}
this.bottomRailActive = true;
try {
window.ramp.que.push(() => {
try {
window.ramp.spaAddAds([{ type: "bottom_rail" }]);
console.log("Bottom rail ad loaded");
} catch (e) {
console.error("Failed to add bottom_rail ad:", e);
}
});
} catch (error) {
console.error("Failed to load bottom_rail ad:", error);
}
}
public destroyBottomRail(): void {
if (!this.bottomRailActive) return;
this.bottomRailActive = false;
if (!window.ramp) return;
try {
window.ramp.destroyUnits("pw-oop-bottom_rail");
console.log("Bottom rail ad destroyed");
} catch (e) {
console.error("Error destroying bottom_rail ad:", e);
}
}
private loadGutterAds(): void {
console.log("loading ramp gutter ads");
const leftContainer = this.querySelector(`#${this.leftContainerId}`);
@@ -149,10 +186,7 @@ export class HomepagePromos extends LitElement {
<!-- Left Gutter Ad -->
<div
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-40 pointer-events-auto items-center justify-center xl:[--half-content:10.5cm] 2xl:[--half-content:12.5cm]"
style="left: calc(50% - var(--half-content) - 208px); top: calc(50% + 10px${this
.hasFooterAd
? " - 1.2cm"
: ""});"
style="left: calc(50% - var(--half-content) - 208px); top: calc(50% + 10px);"
>
<div
id="${this.leftContainerId}"
@@ -163,10 +197,7 @@ export class HomepagePromos extends LitElement {
<!-- Right Gutter Ad -->
<div
class="hidden xl:flex fixed transform -translate-y-1/2 w-[160px] min-h-[600px] z-40 pointer-events-auto items-center justify-center xl:[--half-content:10.5cm] 2xl:[--half-content:12.5cm]"
style="left: calc(50% + var(--half-content) + 48px); top: calc(50% + 10px${this
.hasFooterAd
? " - 1.2cm"
: ""});"
style="left: calc(50% + var(--half-content) + 48px); top: calc(50% + 10px);"
>
<div
id="${this.rightContainerId}"
@@ -176,84 +207,3 @@ export class HomepagePromos extends LitElement {
`;
}
}
// ─── Footer Ad ───────────────────────────────────────────────────────────────
@customElement("home-footer-ad")
export class HomeFooterAd extends LitElement {
@state() private shouldShow: boolean = false;
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "contents";
document.addEventListener("userMeResponse", this.onUserMeResponse);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("userMeResponse", this.onUserMeResponse);
this.destroyAd();
}
private onUserMeResponse = () => {
const isDesktop = window.innerWidth >= 640;
if (
!window.adsEnabled ||
(isDesktop && window.innerHeight < FOOTER_AD_MIN_HEIGHT)
) {
return;
}
this.shouldShow = true;
this.updateComplete.then(() => {
this.loadAd();
});
};
private loadAd(): void {
if (!window.ramp) {
console.warn("Playwire RAMP not available for footer ad");
return;
}
try {
window.ramp.que.push(() => {
try {
window.ramp.spaAddAds([
{ type: FOOTER_AD_TYPE, selectorId: FOOTER_AD_CONTAINER_ID },
]);
console.log("Footer ad loaded:", FOOTER_AD_TYPE);
} catch (e) {
console.error("Failed to add footer ad:", e);
}
});
} catch (error) {
console.error("Failed to load footer ad:", error);
}
}
private destroyAd(): void {
try {
window.ramp.destroyUnits(FOOTER_AD_TYPE);
console.log("successfully destroyed footer ad");
} catch (e) {
console.error("error destroying footer ad", e);
}
}
render() {
if (!this.shouldShow) {
return nothing;
}
return html`
<div
id="${FOOTER_AD_CONTAINER_ID}"
class="flex justify-center items-center w-full pointer-events-auto [&_*]:!m-0 [&_*]:!p-0"
style="margin: 0; padding: 0; line-height: 0;"
></div>
`;
}
}
+2 -5
View File
@@ -32,7 +32,7 @@ import { GameModeSelector } from "./GameModeSelector";
import { GameStartingModal } from "./GameStartingModal";
import "./GoogleAdElement";
import { HelpModal } from "./HelpModal";
import { HomepagePromos } from "./HomepagePromos";
import "./HomepagePromos";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinLobbyModal } from "./JoinLobbyModal";
import "./LangSelector";
@@ -62,6 +62,7 @@ import {
isInIframe,
translateText,
} from "./Utils";
import "./components/DesktopNavBar";
import "./components/Footer";
import "./components/MainLayout";
@@ -307,10 +308,6 @@ class Client {
}
});
const gutterAds = document.querySelector("homepage-promos");
if (!(gutterAds instanceof HomepagePromos))
throw new Error("Missing homepage-promos");
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
+1 -1
View File
@@ -150,7 +150,7 @@ export class TerritoryPatternsModal extends BaseModal {
</div>
<div class="flex justify-center py-3 shrink-0">
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wider rounded-lg bg-blue-600 hover:bg-blue-700 text-white cursor-pointer transition-colors"
class="no-crazygames px-4 py-2 text-sm font-bold uppercase tracking-wider rounded-lg bg-blue-600 hover:bg-blue-700 text-white cursor-pointer transition-colors"
@click=${() => {
this.close();
window.showPage?.("page-item-store");
+64 -51
View File
@@ -4,29 +4,32 @@ import { GameView } from "../../../core/game/GameView";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { Layer } from "./Layer";
const AD_TYPE = "standard_iab_left1";
const AD_CONTAINER_ID = "in-game-bottom-left-ad";
const BOTTOM_RAIL_TYPE = "bottom_rail";
const AD_TYPES = [
{ type: "standard_iab_left1", selectorId: "in-game-bottom-left-ad" },
{ type: "standard_iab_left3", selectorId: "in-game-bottom-left-ad3" },
{ type: "standard_iab_left4", selectorId: "in-game-bottom-left-ad4" },
];
@customElement("in-game-promo")
export class InGamePromo extends LitElement implements Layer {
public game: GameView;
private shouldShow: boolean = false;
private bottomRailActive: boolean = false;
private adsVisible: boolean = false;
private bottomRailDestroyed: boolean = false;
private cornerAdShown: boolean = false;
private adCheckInterval: ReturnType<typeof setTimeout> | null = null;
createRenderRoot() {
return this;
}
init() {
this.showBottomRail();
}
init() {}
tick() {
if (!this.game.inSpawnPhase()) {
if (this.bottomRailActive) {
if (!this.bottomRailDestroyed) {
this.bottomRailDestroyed = true;
this.destroyBottomRail();
}
if (!this.cornerAdShown) {
@@ -37,38 +40,12 @@ export class InGamePromo extends LitElement implements Layer {
}
}
private showBottomRail(): void {
if (!window.adsEnabled) return;
if (!this.game.inSpawnPhase()) return;
if (!window.ramp) {
console.warn("Playwire RAMP not available for bottom_rail ad");
return;
}
this.bottomRailActive = true;
try {
window.ramp.que.push(() => {
try {
window.ramp.spaAddAds([{ type: BOTTOM_RAIL_TYPE }]);
console.log("Bottom rail ad loaded during spawn phase");
} catch (e) {
console.error("Failed to add bottom_rail ad:", e);
}
});
} catch (error) {
console.error("Failed to load bottom_rail ad:", error);
}
}
private destroyBottomRail(): void {
if (!this.bottomRailActive) return;
this.bottomRailActive = false;
if (!window.ramp) return;
try {
window.ramp.spaAds({ ads: [], countPageview: false });
console.log("Bottom rail ad destroyed via spaAds after spawn phase");
window.ramp.destroyUnits("pw-oop-bottom_rail");
console.log("Bottom rail ad destroyed after spawn phase");
} catch (e) {
console.error("Error destroying bottom_rail ad:", e);
}
@@ -93,6 +70,7 @@ export class InGamePromo extends LitElement implements Layer {
this.updateComplete.then(() => {
this.loadAd();
this.checkForAds();
});
}
@@ -115,6 +93,26 @@ export class InGamePromo extends LitElement implements Layer {
});
}
private checkForAds(): void {
if (this.adCheckInterval) {
clearInterval(this.adCheckInterval);
}
this.adCheckInterval = setInterval(() => {
const hasAds = AD_TYPES.some(({ selectorId }) => {
const el = document.getElementById(selectorId);
return el && el.clientHeight > 50;
});
if (hasAds) {
this.adsVisible = true;
this.requestUpdate();
if (this.adCheckInterval) {
clearInterval(this.adCheckInterval);
this.adCheckInterval = null;
}
}
}, 1000);
}
private loadAd(): void {
if (!window.ramp) {
console.warn("Playwire RAMP not available for in-game ad");
@@ -124,23 +122,28 @@ export class InGamePromo extends LitElement implements Layer {
try {
window.ramp.que.push(() => {
try {
window.ramp.spaAddAds([
{
type: AD_TYPE,
selectorId: AD_CONTAINER_ID,
},
]);
console.log("In-game bottom-left ad loaded:", AD_TYPE);
window.ramp.spaAddAds(
AD_TYPES.map(({ type, selectorId }) => ({ type, selectorId })),
);
console.log(
"In-game bottom-left ads loaded:",
AD_TYPES.map((a) => a.type),
);
} catch (e) {
console.error("Failed to add in-game ad:", e);
console.error("Failed to add in-game ads:", e);
}
});
} catch (error) {
console.error("Failed to load in-game ad:", error);
console.error("Failed to load in-game ads:", error);
}
}
public hideAd(): void {
if (this.adCheckInterval) {
clearInterval(this.adCheckInterval);
this.adCheckInterval = null;
}
this.adsVisible = false;
this.destroyBottomRail();
if (crazyGamesSDK.isOnCrazyGames()) {
@@ -156,10 +159,12 @@ export class InGamePromo extends LitElement implements Layer {
}
this.shouldShow = false;
try {
window.ramp.destroyUnits(AD_TYPE);
console.log("successfully destroyed in-game bottom-left ad");
for (const { type } of AD_TYPES) {
window.ramp.destroyUnits(type);
}
console.log("successfully destroyed in-game bottom-left ads");
} catch (e) {
console.error("error destroying in-game ad:", e);
console.error("error destroying in-game ads:", e);
}
this.requestUpdate();
}
@@ -175,10 +180,18 @@ export class InGamePromo extends LitElement implements Layer {
return html`
<div
id="${AD_CONTAINER_ID}"
class="fixed left-0 z-[100] pointer-events-auto"
id="in-game-promo-container"
class="fixed left-0 z-[100] pointer-events-auto flex flex-col-reverse ${this
.adsVisible
? "bg-gray-800 rounded-tr-lg p-1"
: ""}"
style="bottom: -0.7cm"
></div>
>
${AD_TYPES.map(
({ selectorId }) =>
html`<div id="${selectorId}" style="margin:0;padding:0"></div>`,
)}
</div>
`;
}
}
+5
View File
@@ -43,6 +43,11 @@ export const TokenPayloadSchema = z.object({
iss: z.string(),
aud: z.string(),
exp: z.number(),
role: z
.enum(["root", "admin", "mod", "flagged", "banned"])
// In case new roles are added in the future.
.or(z.string())
.optional(),
});
export type TokenPayload = z.infer<typeof TokenPayloadSchema>;
+19 -5
View File
@@ -116,15 +116,29 @@ function censorWithMatcher(
matcher: RegExpMatcher,
): { username: string; clanTag: string | null } {
const usernameIsProfane = matcher.hasMatch(username);
const censoredName = usernameIsProfane
? shadowNames[simpleHash(username) % shadowNames.length]
: username;
const clanTagIsProfane = clanTag
? matcher.hasMatch(clanTag) || clanTag.toLowerCase() === "ss"
: false;
// Catch slurs split across clan tag and username (e.g. clanTag="HIT", username="LER")
// by looking for a match that spans the clan/name boundary.
const combinedSlurAcrossBoundary = clanTag
? matcher.getAllMatches(clanTag + username).some(
(match) =>
// Match must start in the clan and extend into the name — otherwise
// it's already handled by the clan-only or name-only checks above.
match.startIndex < clanTag.length && match.endIndex >= clanTag.length,
)
: false;
const censoredName =
usernameIsProfane || combinedSlurAcrossBoundary
? shadowNames[simpleHash(username) % shadowNames.length]
: username;
const censoredClanTag =
clanTag && !clanTagIsProfane ? clanTag.toUpperCase() : null;
clanTag && !clanTagIsProfane && !combinedSlurAcrossBoundary
? clanTag.toUpperCase()
: null;
return { username: censoredName, clanTag: censoredClanTag };
}
+5
View File
@@ -357,6 +357,11 @@ export async function startWorker() {
}
const { persistentId, claims } = result;
if (claims?.role === "banned") {
ws.close(1002, "Account Banned");
return;
}
if (clientMsg.type === "rejoin") {
log.info("rejoining game", {
gameID: clientMsg.gameID,
+38
View File
@@ -230,6 +230,44 @@ describe("UsernameCensor", () => {
expect(result.clanTag).toBeNull();
expect(shadowNames).toContain(result.username);
});
describe("clan tag + username combined forms a slur", () => {
test("censors when clan+name combined forms hitler", () => {
const result = checker.censor("LER", "HIT");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
test("censors when clan+name combined forms hitler (split differently)", () => {
const result = checker.censor("TLER", "HI");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
test("censors when clan+name combined forms adolf", () => {
const result = checker.censor("OLF", "AD");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
test("censors when clan+name combined forms nigger", () => {
const result = checker.censor("ger", "NIG");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
test("censors when clan+name combined forms nigger (clean parts)", () => {
const result = checker.censor("gger", "NI");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
test("censors leet speak combined across clan and name", () => {
const result = checker.censor("g3r", "N1G");
expect(shadowNames).toContain(result.username);
expect(result.clanTag).toBeNull();
});
});
});
test("returns deterministic shadow name for same input", () => {