Replace @zestyxyz/babylonjs-sdk with local ES-module ZestyBanner
All checks were successful
Build / build (push) Successful in 1m46s
All checks were successful
Build / build (push) Successful in 1m46s
The SDK referenced a global BABYLON namespace which broke ES module imports. Rewrote as three local modules (zestyBanner, zestyNetworking, zestyFormats) using proper named imports from @babylonjs/core, with safe camera access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ce2b86569
commit
a6715c62bc
77
src/ads/zestyBanner.ts
Normal file
77
src/ads/zestyBanner.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
MeshBuilder, ActionManager, ExecuteCodeAction,
|
||||||
|
StandardMaterial, Texture, WebXRState,
|
||||||
|
} from '@babylonjs/core';
|
||||||
|
import type { Scene, Mesh, WebXRDefaultExperience } from '@babylonjs/core';
|
||||||
|
import { formats } from './zestyFormats';
|
||||||
|
import {
|
||||||
|
fetchCampaignAd, sendOnLoadMetric, sendOnClickMetric,
|
||||||
|
AD_REFRESH_INTERVAL,
|
||||||
|
} from './zestyNetworking';
|
||||||
|
import type { AdData } from './zestyNetworking';
|
||||||
|
|
||||||
|
function getCamera(scene: Scene, xr?: WebXRDefaultExperience) {
|
||||||
|
if (xr?.baseExperience?.state === WebXRState.IN_XR) {
|
||||||
|
return xr.baseExperience.camera;
|
||||||
|
}
|
||||||
|
return scene.cameras.length > 0 ? scene.cameras[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAd(
|
||||||
|
banner: Mesh, scene: Scene, adUnitId: string,
|
||||||
|
format: string, adRef: { current: AdData }
|
||||||
|
): Promise<void> {
|
||||||
|
const ad = await fetchCampaignAd(adUnitId, format);
|
||||||
|
adRef.current = ad;
|
||||||
|
const mat = banner.material as StandardMaterial;
|
||||||
|
mat.diffuseTexture?.dispose();
|
||||||
|
const tex = new Texture(ad.assetUrl, scene);
|
||||||
|
tex.hasAlpha = true;
|
||||||
|
mat.diffuseTexture = tex;
|
||||||
|
if (ad.campaignId) sendOnLoadMetric(adUnitId, ad.campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUrl(url: string): void {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createZestyBanner(
|
||||||
|
adUnitId: string, format: string, height: number,
|
||||||
|
scene: Scene, xr?: WebXRDefaultExperience
|
||||||
|
): Mesh {
|
||||||
|
const fmt = formats[format] ?? formats['billboard'];
|
||||||
|
const planeOpts = { width: fmt.width * height, height };
|
||||||
|
const banner = MeshBuilder.CreatePlane('zestybanner', planeOpts, scene);
|
||||||
|
|
||||||
|
const mat = new StandardMaterial('zestyMat', scene);
|
||||||
|
mat.diffuseTexture = new Texture(fmt.defaultImage, scene);
|
||||||
|
(mat.diffuseTexture as Texture).hasAlpha = true;
|
||||||
|
banner.material = mat;
|
||||||
|
|
||||||
|
const adRef = { current: { assetUrl: '', ctaUrl: '', campaignId: '' } };
|
||||||
|
loadAd(banner, scene, adUnitId, format, adRef);
|
||||||
|
|
||||||
|
banner.actionManager = new ActionManager(scene);
|
||||||
|
banner.actionManager.registerAction(
|
||||||
|
new ExecuteCodeAction(ActionManager.OnPickTrigger, () => {
|
||||||
|
if (!adRef.current.ctaUrl) return;
|
||||||
|
if (xr?.baseExperience) {
|
||||||
|
xr.baseExperience.sessionManager.exitXRAsync()
|
||||||
|
.then(() => openUrl(adRef.current.ctaUrl));
|
||||||
|
} else {
|
||||||
|
openUrl(adRef.current.ctaUrl);
|
||||||
|
}
|
||||||
|
if (adRef.current.campaignId) {
|
||||||
|
sendOnClickMetric(adUnitId, adRef.current.campaignId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const cam = getCamera(scene, xr);
|
||||||
|
if (!cam || !scene.isActiveMesh(banner)) return;
|
||||||
|
loadAd(banner, scene, adUnitId, format, adRef);
|
||||||
|
}, AD_REFRESH_INTERVAL);
|
||||||
|
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
20
src/ads/zestyFormats.ts
Normal file
20
src/ads/zestyFormats.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const CDN_BASE = 'https://cdn.zesty.xyz/sdk/assets';
|
||||||
|
|
||||||
|
export interface FormatInfo {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
defaultImage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formats: Record<string, FormatInfo> = {
|
||||||
|
billboard: {
|
||||||
|
width: 3.88,
|
||||||
|
height: 1,
|
||||||
|
defaultImage: `${CDN_BASE}/zesty-default-billboard.png`,
|
||||||
|
},
|
||||||
|
'medium-rectangle': {
|
||||||
|
width: 1.2,
|
||||||
|
height: 1,
|
||||||
|
defaultImage: `${CDN_BASE}/zesty-default-medium-rectangle.png`,
|
||||||
|
},
|
||||||
|
};
|
||||||
78
src/ads/zestyNetworking.ts
Normal file
78
src/ads/zestyNetworking.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { formats } from './zestyFormats';
|
||||||
|
|
||||||
|
const API_BASE = 'https://api.zesty.market/api';
|
||||||
|
const GRAPHQL_URL = 'https://market.zesty.xyz/graphql';
|
||||||
|
|
||||||
|
export const AD_REFRESH_INTERVAL = 30000;
|
||||||
|
|
||||||
|
export interface AdData {
|
||||||
|
assetUrl: string;
|
||||||
|
ctaUrl: string;
|
||||||
|
campaignId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPlatform(): { name: string; confidence: string } {
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
if (/OculusBrowser/i.test(ua)) return { name: 'Oculus', confidence: 'high' };
|
||||||
|
if (/Wolvic/i.test(ua)) return { name: 'Wolvic', confidence: 'high' };
|
||||||
|
if (/Pico/i.test(ua)) return { name: 'Pico', confidence: 'medium' };
|
||||||
|
return { name: 'Desktop', confidence: 'low' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCampaignAd(
|
||||||
|
adUnitId: string, format: string
|
||||||
|
): Promise<AdData> {
|
||||||
|
try {
|
||||||
|
const url = encodeURI(window.location.href).replace(/\/$/, '');
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE}/ad?ad_unit_id=${adUnitId}&url=${url}`
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.Ads?.length) {
|
||||||
|
return {
|
||||||
|
assetUrl: json.Ads[0].asset_url,
|
||||||
|
ctaUrl: json.Ads[0].cta_url,
|
||||||
|
campaignId: json.CampaignId ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('Zesty: failed to fetch ad, using default');
|
||||||
|
}
|
||||||
|
const fmt = formats[format] ?? formats['billboard'];
|
||||||
|
return { assetUrl: fmt.defaultImage, ctaUrl: '', campaignId: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMetric(
|
||||||
|
eventType: string, adUnitId: string, campaignId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const { name, confidence } = detectPlatform();
|
||||||
|
const query = `mutation { increment(
|
||||||
|
eventType: ${eventType},
|
||||||
|
spaceId: "${adUnitId}",
|
||||||
|
campaignId: "${campaignId}",
|
||||||
|
platform: { name: ${name}, confidence: ${confidence} }
|
||||||
|
) { message } }`;
|
||||||
|
try {
|
||||||
|
await fetch(GRAPHQL_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ query }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
console.warn(`Zesty: failed to send ${eventType} metric`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendOnLoadMetric(
|
||||||
|
adUnitId: string, campaignId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await sendMetric('visits', adUnitId, campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendOnClickMetric(
|
||||||
|
adUnitId: string, campaignId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await sendMetric('clicks', adUnitId, campaignId);
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import {
|
|||||||
import { DefaultScene } from "./defaultScene";
|
import { DefaultScene } from "./defaultScene";
|
||||||
import { InputControlManager } from "../ship/input/inputControlManager";
|
import { InputControlManager } from "../ship/input/inputControlManager";
|
||||||
import log from './logger';
|
import log from './logger';
|
||||||
|
import { createZestyBanner } from '../ads/zestyBanner';
|
||||||
|
|
||||||
const XR_RENDERING_GROUP = 3;
|
const XR_RENDERING_GROUP = 3;
|
||||||
const FADE_DELAY_MS = 500;
|
const FADE_DELAY_MS = 500;
|
||||||
@ -54,7 +55,18 @@ async function createXRExperience(): Promise<void> {
|
|||||||
disablePointerSelection: true // Disable to re-enable with custom options
|
disablePointerSelection: true // Disable to re-enable with custom options
|
||||||
});
|
});
|
||||||
log.debug(WebXRFeaturesManager.GetAvailableFeatures());
|
log.debug(WebXRFeaturesManager.GetAvailableFeatures());
|
||||||
|
try {
|
||||||
|
const banner = createZestyBanner("a2170882-f232-4da0-9315-747ee049e642",
|
||||||
|
"billboard", 10, DefaultScene.MainScene, DefaultScene.XR);
|
||||||
|
banner.position.z = 50;
|
||||||
|
banner.position.y = 5;
|
||||||
|
banner.rotation.y = Math.PI;
|
||||||
|
banner.renderOverlay = true;
|
||||||
|
banner.overlayColor = Color3.Red();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.debug("Zesty banner init failed:", e);
|
||||||
|
}
|
||||||
// Enable pointer selection with renderingGroupId so laser is never occluded
|
// Enable pointer selection with renderingGroupId so laser is never occluded
|
||||||
DefaultScene.XR.baseExperience.featuresManager.enableFeature(
|
DefaultScene.XR.baseExperience.featuresManager.enableFeature(
|
||||||
WebXRFeatureName.POINTER_SELECTION,
|
WebXRFeatureName.POINTER_SELECTION,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user