@genex-ai/cli-demo 0.71.0 → 0.74.0-dev.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +354 -8
- package/package.json +2 -1
- package/templates/controllers/character/follow-camera.ts +15 -4
- package/templates/controllers/character/vrm/vrm-loader.ts +74 -11
- package/templates/controllers/quality/governor.ts +147 -0
- package/templates/controllers/quality/pick-asset.ts +57 -0
- package/templates/controllers/quality/tier.ts +170 -0
- package/templates/skills/genex-ai-hud/SKILL.md +11 -2
- package/templates/skills/genex-ai-menu/SKILL.md +18 -2
- package/templates/skills/genex-ai-skybox/SKILL.md +15 -4
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-explore/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +141 -0
- package/templates/skills/genex-threejs-adaptive-quality/references/adaptive-quality.md +105 -0
- package/templates/skills/genex-threejs-bloom/SKILL.md +4 -1
- package/templates/skills/genex-threejs-bloom/references/bloom.md +1 -1
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +62 -12
- package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +62 -0
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +9 -3
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +4 -1
- package/templates/skills/genex-threejs-game-feel/SKILL.md +4 -1
- package/templates/skills/genex-threejs-game-ui/SKILL.md +113 -30
- package/templates/skills/genex-threejs-game-ui/references/style-capsules.md +4 -1
- package/templates/skills/genex-threejs-image-pipeline/SKILL.md +5 -0
- package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +1 -1
- package/templates/skills/genex-threejs-lighting-design/SKILL.md +5 -1
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +7 -1
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +6 -3
- package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +1 -0
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +1 -1
- package/templates/skills/genex-threejs-shadow-systems/SKILL.md +6 -0
- package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +1 -1
- package/templates/skills/genex-threejs-skill-router/SKILL.md +26 -5
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +25 -9
- package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +1 -1
- package/templates/skills/genex-threejs-touch-controls/SKILL.md +11 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +7 -0
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +36 -12
- package/templates/skills/genex-threejs-water-optics/references/water-optics.md +1 -1
- package/templates/skills/genex-updates/SKILL.md +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Genex adaptive-quality: the runtime governor (mobile-readiness program).
|
|
2
|
+
// Tiers pick the START; the governor owns the RUNTIME knobs forever — phones
|
|
3
|
+
// thermally throttle after minutes, so a game that benched fine at 0:30
|
|
4
|
+
// degrades at 8:00. Feed it a frame time every frame; wire the callbacks to
|
|
5
|
+
// the runtime-changeable knobs only (DPR, post passes, draw distance, frame
|
|
6
|
+
// cap — NEVER context-creation flags like antialias, those are fixed).
|
|
7
|
+
//
|
|
8
|
+
// Step-down ladder on sustained slowness: DPR ×0.8 → post off → draw distance
|
|
9
|
+
// down → 30fps cap. Step-up is slow (hysteresis) and a step that failed twice
|
|
10
|
+
// is never re-attempted. It also self-reports renderer.info counts to
|
|
11
|
+
// window.__GENEX_QUALITY__ — the crash watchdog attaches them to its beacons,
|
|
12
|
+
// which is how the field data that calibrates the whole program gets its
|
|
13
|
+
// memory signal.
|
|
14
|
+
import type { QualityTier } from './tier.ts';
|
|
15
|
+
|
|
16
|
+
export interface GovernorCallbacks {
|
|
17
|
+
/** Apply a DPR multiplier (1 = tier cap). E.g. renderer.setPixelRatio(Math.min(realDpr, tier.dprCap * m)). */
|
|
18
|
+
setDprScale?: (multiplier: number) => void;
|
|
19
|
+
/** Toggle the post stack between the tier's level and 'off'. */
|
|
20
|
+
setPostEnabled?: (enabled: boolean) => void;
|
|
21
|
+
/** Apply a draw-distance multiplier (1 = tier scale). */
|
|
22
|
+
setDrawDistanceScale?: (multiplier: number) => void;
|
|
23
|
+
/** Apply a frame cap (0 = uncapped). Pace to a STABLE 30 over a stuttery 45. */
|
|
24
|
+
setFrameCap?: (fps: number) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RendererInfoLike {
|
|
28
|
+
info?: { memory?: { textures?: number; geometries?: number } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SLOW_WINDOW_MS = 4000; // sustained, not spikes — shader compiles must not trigger steps
|
|
32
|
+
const RECOVER_WINDOW_MS = 20000; // step up slowly (hysteresis)
|
|
33
|
+
const MEM_REPORT_MS = 5000;
|
|
34
|
+
|
|
35
|
+
export class QualityGovernor {
|
|
36
|
+
private tier: QualityTier;
|
|
37
|
+
private steps: Array<{ apply: () => void; revert: () => void; failures: number; pendingRecovery?: boolean }>;
|
|
38
|
+
private applied = 0;
|
|
39
|
+
private slowSince: number | null = null;
|
|
40
|
+
private goodSince: number | null = null;
|
|
41
|
+
private lastMemReport = 0;
|
|
42
|
+
private paused = false;
|
|
43
|
+
|
|
44
|
+
constructor(tier: QualityTier, callbacks: GovernorCallbacks, renderer?: RendererInfoLike) {
|
|
45
|
+
this.tier = tier;
|
|
46
|
+
this.rendererRef = renderer;
|
|
47
|
+
const c = callbacks;
|
|
48
|
+
this.steps = [
|
|
49
|
+
{
|
|
50
|
+
apply: () => c.setDprScale?.(0.8),
|
|
51
|
+
revert: () => c.setDprScale?.(1),
|
|
52
|
+
failures: 0,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
apply: () => c.setPostEnabled?.(false),
|
|
56
|
+
revert: () => c.setPostEnabled?.(true),
|
|
57
|
+
failures: 0,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
apply: () => c.setDrawDistanceScale?.(0.6),
|
|
61
|
+
revert: () => c.setDrawDistanceScale?.(1),
|
|
62
|
+
failures: 0,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
apply: () => c.setFrameCap?.(30),
|
|
66
|
+
revert: () => c.setFrameCap?.(this.tier.frameCap),
|
|
67
|
+
failures: 0,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
// Visibility lifecycle: a backgrounded game must stop burning GPU/audio on
|
|
71
|
+
// exactly the thermally-constrained device class. The game's loop should
|
|
72
|
+
// also pause itself; the governor at minimum stops judging frames.
|
|
73
|
+
try {
|
|
74
|
+
document.addEventListener('visibilitychange', () => {
|
|
75
|
+
this.paused = document.visibilityState !== 'visible';
|
|
76
|
+
this.slowSince = null;
|
|
77
|
+
this.goodSince = null;
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
/* non-DOM context */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private rendererRef?: RendererInfoLike;
|
|
85
|
+
|
|
86
|
+
/** Call once per frame with the frame delta in ms (performance.now() based). */
|
|
87
|
+
frame(deltaMs: number): void {
|
|
88
|
+
if (this.paused) return;
|
|
89
|
+
const now = performance.now();
|
|
90
|
+
const budgetMs = 1000 / Math.min(this.tier.frameCap, 60) + 8; // ~24ms at 30, ~25ms at 60
|
|
91
|
+
|
|
92
|
+
if (deltaMs > budgetMs) {
|
|
93
|
+
this.goodSince = null;
|
|
94
|
+
if (this.slowSince == null) this.slowSince = now;
|
|
95
|
+
else if (now - this.slowSince > SLOW_WINDOW_MS) {
|
|
96
|
+
this.stepDown();
|
|
97
|
+
this.slowSince = null;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
this.slowSince = null;
|
|
101
|
+
if (this.goodSince == null) this.goodSince = now;
|
|
102
|
+
else if (now - this.goodSince > RECOVER_WINDOW_MS) {
|
|
103
|
+
this.stepUp();
|
|
104
|
+
this.goodSince = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Memory self-report for the crash watchdog's beacons (field calibration).
|
|
109
|
+
if (now - this.lastMemReport > MEM_REPORT_MS) {
|
|
110
|
+
this.lastMemReport = now;
|
|
111
|
+
try {
|
|
112
|
+
const mem = this.rendererRef?.info?.memory;
|
|
113
|
+
if (mem) {
|
|
114
|
+
(window as Window & { __GENEX_QUALITY__?: { tex?: number; geo?: number } }).__GENEX_QUALITY__ =
|
|
115
|
+
{ tex: mem.textures ?? 0, geo: mem.geometries ?? 0 };
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
/* observational only */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private stepDown(): void {
|
|
124
|
+
if (this.applied >= this.steps.length) return;
|
|
125
|
+
const step = this.steps[this.applied]!;
|
|
126
|
+
// A re-application of a rung whose recovery is still pending means the
|
|
127
|
+
// revert did NOT hold — that's the failure being counted, never the
|
|
128
|
+
// successful revert itself.
|
|
129
|
+
if (step.pendingRecovery) {
|
|
130
|
+
step.pendingRecovery = false;
|
|
131
|
+
step.failures++;
|
|
132
|
+
}
|
|
133
|
+
step.apply();
|
|
134
|
+
this.applied++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private stepUp(): void {
|
|
138
|
+
if (this.applied === 0) return;
|
|
139
|
+
const step = this.steps[this.applied - 1]!;
|
|
140
|
+
// Never re-attempt a knob whose recovery failed twice (reverting it put
|
|
141
|
+
// the game back over budget both times) — it stays applied for the session.
|
|
142
|
+
if (step.failures >= 2) return;
|
|
143
|
+
step.pendingRecovery = true;
|
|
144
|
+
step.revert();
|
|
145
|
+
this.applied--;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Genex adaptive-quality: per-tier asset variant selection (mobile-readiness
|
|
2
|
+
// program, Option A+D). Generated assets ship with GUARANTEED downscale rungs
|
|
3
|
+
// in R2 — sibling roles like "skybox-equirect@2048" — so phones can load a
|
|
4
|
+
// ~11 MB sky instead of the 8192x4096 original (~178 MB decoded). Desktop
|
|
5
|
+
// always loads the bare URL: the ladder exists so phones stop dying, never to
|
|
6
|
+
// make desktops uglier.
|
|
7
|
+
//
|
|
8
|
+
// FALLBACK CONTRACT: a rung can be missing (remixed old URLs, an environment
|
|
9
|
+
// whose backfill hasn't run). loadTextureWithFallback retries the bare URL on
|
|
10
|
+
// a rung failure, so the worst case is today's behavior — never a broken boot.
|
|
11
|
+
import type { QualityTier } from './tier.ts';
|
|
12
|
+
|
|
13
|
+
const GENEX_GENERATIONS_RE = /^https:\/\/assets\.genex\.technology\/generations\/[^/]+\/[^/@]+$/;
|
|
14
|
+
|
|
15
|
+
/** Rung widths by role family — mirrors the server's ladder (store.ts MOBILE_RUNGS). */
|
|
16
|
+
function rungWidthFor(url: string, tier: QualityTier): number | null {
|
|
17
|
+
if (tier.name !== 'phone' && tier.name !== 'phone-low') return null;
|
|
18
|
+
const role = url.split('/').pop() ?? '';
|
|
19
|
+
if (role === 'skybox-equirect') return tier.name === 'phone-low' ? 2048 : 4096;
|
|
20
|
+
if (role === 'texture-basecolor' || role === 'image-main' || /^image-alt-\d+$/.test(role)) {
|
|
21
|
+
return tier.name === 'phone-low' ? 1024 : 2048;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the URL a THIS-tier device should load. Non-generated URLs and
|
|
28
|
+
* desktop tiers pass through untouched; phone tiers get the rung sibling.
|
|
29
|
+
*/
|
|
30
|
+
export function pickAsset(url: string, tier: QualityTier): string {
|
|
31
|
+
if (!GENEX_GENERATIONS_RE.test(url)) return url;
|
|
32
|
+
const width = rungWidthFor(url, tier);
|
|
33
|
+
return width ? `${url}@${width}` : url;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load an image URL through the ladder with the bare-URL fallback. Use it for
|
|
38
|
+
* generated skyboxes/textures instead of raw TextureLoader.loadAsync:
|
|
39
|
+
*
|
|
40
|
+
* const texture = await loadTextureWithFallback(
|
|
41
|
+
* SKYBOX_URL, tier, (u) => new THREE.TextureLoader().loadAsync(u),
|
|
42
|
+
* );
|
|
43
|
+
*/
|
|
44
|
+
export async function loadTextureWithFallback<T>(
|
|
45
|
+
url: string,
|
|
46
|
+
tier: QualityTier,
|
|
47
|
+
load: (resolvedUrl: string) => Promise<T>,
|
|
48
|
+
): Promise<T> {
|
|
49
|
+
const picked = pickAsset(url, tier);
|
|
50
|
+
if (picked === url) return load(url);
|
|
51
|
+
try {
|
|
52
|
+
return await load(picked);
|
|
53
|
+
} catch {
|
|
54
|
+
// Missing rung (old asset, un-backfilled env) — degrade to the original.
|
|
55
|
+
return load(url);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Genex adaptive-quality: device-tier detection (mobile-readiness program).
|
|
2
|
+
// Boot-conservative by design — on phones you can recover from ugly, you
|
|
3
|
+
// cannot recover from a jetsam kill, and boot is exactly when iOS kills. The
|
|
4
|
+
// governor (governor.ts) steps quality UP after smooth seconds, so a
|
|
5
|
+
// too-cautious start costs moments of softness, never a crash.
|
|
6
|
+
//
|
|
7
|
+
// Detection honesty: no single signal is trustworthy. iOS masks the WebGL
|
|
8
|
+
// renderer string to "Apple GPU" (screen+DPR+OS-version is the real Apple
|
|
9
|
+
// signal); Android exposes detailed renderer strings (Adreno/Mali) worth a
|
|
10
|
+
// small lookup; the runtime governor measures actual frame times and corrects
|
|
11
|
+
// both directions. All heuristics here only pick the STARTING tier.
|
|
12
|
+
|
|
13
|
+
export type TierName = 'phone-low' | 'phone' | 'desktop' | 'desktop-high';
|
|
14
|
+
|
|
15
|
+
export interface QualityTier {
|
|
16
|
+
name: TierName;
|
|
17
|
+
/** Cap for renderer.setPixelRatio(Math.min(devicePixelRatio, dprCap)). */
|
|
18
|
+
dprCap: number;
|
|
19
|
+
/** WebGL context antialias (context-creation-FIXED — cannot change live). */
|
|
20
|
+
antialias: boolean;
|
|
21
|
+
/** Directional/spot shadow map size (0 = shadows off). */
|
|
22
|
+
shadowMapSize: number;
|
|
23
|
+
/** 'off' = tone mapping only; 'light' = +FXAA/vignette; 'full' = the named stack. */
|
|
24
|
+
postLevel: 'off' | 'light' | 'full';
|
|
25
|
+
/** Multiplier for particle counts / scatter density. */
|
|
26
|
+
particleScale: number;
|
|
27
|
+
/** Multiplier for draw distance / fog far. */
|
|
28
|
+
drawDistanceScale: number;
|
|
29
|
+
/** Frame target — phones pace to a STABLE 30 over a stuttery 45. */
|
|
30
|
+
frameCap: number;
|
|
31
|
+
/** Max remote players fully animated/drawn (multiplayer); rest billboard. */
|
|
32
|
+
remoteAvatarCap: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TIERS: Record<TierName, QualityTier> = {
|
|
36
|
+
'phone-low': {
|
|
37
|
+
name: 'phone-low',
|
|
38
|
+
dprCap: 1,
|
|
39
|
+
antialias: false,
|
|
40
|
+
shadowMapSize: 512,
|
|
41
|
+
postLevel: 'off',
|
|
42
|
+
particleScale: 0.25,
|
|
43
|
+
drawDistanceScale: 0.5,
|
|
44
|
+
frameCap: 30,
|
|
45
|
+
remoteAvatarCap: 4,
|
|
46
|
+
},
|
|
47
|
+
phone: {
|
|
48
|
+
name: 'phone',
|
|
49
|
+
dprCap: 1.5,
|
|
50
|
+
antialias: false,
|
|
51
|
+
shadowMapSize: 1024,
|
|
52
|
+
postLevel: 'light',
|
|
53
|
+
particleScale: 0.5,
|
|
54
|
+
drawDistanceScale: 0.75,
|
|
55
|
+
frameCap: 60,
|
|
56
|
+
remoteAvatarCap: 8,
|
|
57
|
+
},
|
|
58
|
+
desktop: {
|
|
59
|
+
name: 'desktop',
|
|
60
|
+
dprCap: 2,
|
|
61
|
+
antialias: true,
|
|
62
|
+
shadowMapSize: 2048,
|
|
63
|
+
postLevel: 'full',
|
|
64
|
+
particleScale: 1,
|
|
65
|
+
drawDistanceScale: 1,
|
|
66
|
+
frameCap: 60,
|
|
67
|
+
remoteAvatarCap: 64,
|
|
68
|
+
},
|
|
69
|
+
'desktop-high': {
|
|
70
|
+
name: 'desktop-high',
|
|
71
|
+
dprCap: 2,
|
|
72
|
+
antialias: true,
|
|
73
|
+
shadowMapSize: 2048,
|
|
74
|
+
postLevel: 'full',
|
|
75
|
+
particleScale: 1,
|
|
76
|
+
drawDistanceScale: 1,
|
|
77
|
+
frameCap: 240,
|
|
78
|
+
remoteAvatarCap: 64,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Quality setting persisted PER DEVICE (localStorage) — quality is a property
|
|
83
|
+
* of the phone, not the player, so it deliberately does not ride account
|
|
84
|
+
* state. 'auto' = heuristics + governor. */
|
|
85
|
+
const SETTING_KEY = 'genex:quality';
|
|
86
|
+
export type QualitySetting = 'auto' | 'low' | 'medium' | 'high';
|
|
87
|
+
|
|
88
|
+
export function getQualitySetting(): QualitySetting {
|
|
89
|
+
try {
|
|
90
|
+
const v = localStorage.getItem(SETTING_KEY);
|
|
91
|
+
if (v === 'low' || v === 'medium' || v === 'high') return v;
|
|
92
|
+
} catch {
|
|
93
|
+
/* storage blocked */
|
|
94
|
+
}
|
|
95
|
+
return 'auto';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setQualitySetting(v: QualitySetting): void {
|
|
99
|
+
try {
|
|
100
|
+
localStorage.setItem(SETTING_KEY, v);
|
|
101
|
+
} catch {
|
|
102
|
+
/* storage blocked */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isTouchDevice(): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const coarse = matchMedia('(pointer: coarse)').matches;
|
|
109
|
+
const touch = navigator.maxTouchPoints > 1;
|
|
110
|
+
const nav = navigator as Navigator & { userAgentData?: { mobile?: boolean } };
|
|
111
|
+
const mobileUA = nav.userAgentData
|
|
112
|
+
? !!nav.userAgentData.mobile
|
|
113
|
+
: /Mobi|Android/i.test(navigator.userAgent);
|
|
114
|
+
return (touch && coarse) || mobileUA;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Android GPU model → strong-enough-for-'phone' (vs 'phone-low'). Tiny,
|
|
121
|
+
* vendored on purpose (no detect-gpu dependency): renderer strings are only
|
|
122
|
+
* meaningful on Android/desktop anyway, and the governor corrects mistakes. */
|
|
123
|
+
const STRONG_ANDROID_GPU = /Adreno \(TM\) [67]\d\d|Adreno \(TM\) 8|Mali-G7[18]|Mali-G[89]\d|Immortalis|Xclipse/i;
|
|
124
|
+
|
|
125
|
+
function androidGpuLooksStrong(): boolean {
|
|
126
|
+
try {
|
|
127
|
+
const canvas = document.createElement('canvas');
|
|
128
|
+
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
|
129
|
+
if (!gl) return false;
|
|
130
|
+
const info = gl.getExtension('WEBGL_debug_renderer_info');
|
|
131
|
+
const renderer = info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : '';
|
|
132
|
+
const ext = gl.getExtension('WEBGL_lose_context');
|
|
133
|
+
if (ext) ext.loseContext(); // free the probe context immediately
|
|
134
|
+
return STRONG_ANDROID_GPU.test(renderer);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Pick the STARTING tier. Manual setting wins; 'auto' uses heuristics:
|
|
142
|
+
* - non-touch → desktop
|
|
143
|
+
* - touch: old OS or small memory signals → phone-low; strong/new → phone
|
|
144
|
+
* The kit may have clamped devicePixelRatio — prefer its pristine stash.
|
|
145
|
+
*/
|
|
146
|
+
export function detectTier(): QualityTier {
|
|
147
|
+
const setting = getQualitySetting();
|
|
148
|
+
if (setting === 'low') return TIERS['phone-low'];
|
|
149
|
+
if (setting === 'medium') return TIERS.phone;
|
|
150
|
+
if (setting === 'high') return TIERS.desktop;
|
|
151
|
+
|
|
152
|
+
if (!isTouchDevice()) return TIERS.desktop;
|
|
153
|
+
|
|
154
|
+
const ua = navigator.userAgent;
|
|
155
|
+
const kit = (window as Window & { __GENEX_KIT__?: { realDpr?: number } }).__GENEX_KIT__;
|
|
156
|
+
const dpr = kit?.realDpr ?? window.devicePixelRatio;
|
|
157
|
+
const isIOS = /iPhone|iPad|iPod/.test(ua) || (navigator.maxTouchPoints > 1 && /Mac/.test(ua));
|
|
158
|
+
|
|
159
|
+
if (isIOS) {
|
|
160
|
+
// iOS version floors device age (iOS 17 ⇒ XS/2018+; screen+DPR refine).
|
|
161
|
+
const m = ua.match(/OS (\d+)_/);
|
|
162
|
+
const major = m ? Number(m[1]) : 0;
|
|
163
|
+
if (major >= 17) return TIERS.phone;
|
|
164
|
+
return TIERS['phone-low'];
|
|
165
|
+
}
|
|
166
|
+
const am = ua.match(/Android (\d+)/);
|
|
167
|
+
const androidMajor = am ? Number(am[1]) : 0;
|
|
168
|
+
if (androidMajor >= 13 && (dpr >= 2.5 || androidGpuLooksStrong())) return TIERS.phone;
|
|
169
|
+
return TIERS['phone-low'];
|
|
170
|
+
}
|
|
@@ -40,7 +40,13 @@ ring) that no hand-written CSS can fake.
|
|
|
40
40
|
What is fixed is WHERE plates are built — the DOM — because
|
|
41
41
|
semi-transparency physically cannot survive sprite extraction: a composited
|
|
42
42
|
panel's pixels are a blend of panel and background, and no cutout can
|
|
43
|
-
un-blend them. Plates are NEVER baked into sprites.
|
|
43
|
+
un-blend them. Plates are NEVER baked into sprites. Give the plate its
|
|
44
|
+
corners with `border-radius` or a clean `clip-path`/`mask` shape — a CSS
|
|
45
|
+
chamfer/notch is allowed, as long as the cut never clips the plate's
|
|
46
|
+
content (text, padding, a glow), leaves no jagged-edge artifacts, and
|
|
47
|
+
nothing ends up crooked; a genuinely ornamented angular frame is still
|
|
48
|
+
best as chrome, generated. The load-bearing `mask`/`clip-path` in this
|
|
49
|
+
skill is the masked-fill reveal below.
|
|
44
50
|
- **Chrome (sprites — what THIS pipeline generates).** Opaque frames, corner
|
|
45
51
|
brackets, ornaments, emblems, icons, medallions — hard-alpha art laid over
|
|
46
52
|
the glass. The Stage-2 sheet contains ONLY chrome; never a panel with its
|
|
@@ -151,7 +157,10 @@ layout come from the element inventory and the game contract — a mechanic
|
|
|
151
157
|
the concept invented (a lap counter, a stamina orb) does not enter the HUD,
|
|
152
158
|
and a mechanic it failed to show still does. Generate 2 mockup candidates in one
|
|
153
159
|
call (`--candidates 2`) and pick the better one: a re-roll costs the whole
|
|
154
|
-
serial chain, a second candidate costs nothing extra in wall-clock.
|
|
160
|
+
serial chain, a second candidate costs nothing extra in wall-clock. If the
|
|
161
|
+
user's concept-loop feedback replaces the anchor frame after Stage 1 ran,
|
|
162
|
+
re-run the pipeline from Stage 1 against the new frame — there is no cheaper
|
|
163
|
+
partial path, so budget the full chain and say so in one line.
|
|
155
164
|
|
|
156
165
|
```bash
|
|
157
166
|
# Stage 1 — full-HUD mockup over a placeholder scene (the LAYOUT REFERENCE).
|
|
@@ -65,7 +65,9 @@ might suggest. Two hard rules for the frame prompt:
|
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
# 1. The frame — a still of the scene the loop returns to (this example is a
|
|
68
|
-
# painterly meadow; YOUR prompt comes from YOUR game's style brief)
|
|
68
|
+
# painterly meadow; YOUR prompt comes from YOUR game's style brief). When the
|
|
69
|
+
# game-ui gate produced a concept frame, anchor the still to it with
|
|
70
|
+
# --edit <concept-url> so menu and world share one palette and light:
|
|
69
71
|
npx genex image "windswept alpine meadow at golden hour, wildflowers leaning in the gusts, painterly light, cinematic wide shot. Edge-to-edge cinematic composition. Full-bleed 16:9 frame. No UI elements, no text, no buttons in the frame." --aspect 16:9 --quality high
|
|
70
72
|
# -> https://assets.genex.technology/generations/<id>/image-main
|
|
71
73
|
|
|
@@ -83,7 +85,13 @@ npx genex wait <gen-id>
|
|
|
83
85
|
|
|
84
86
|
Never wire the clip as a bare `<video loop>` — the residual seam shows every
|
|
85
87
|
cycle. Two stacked `<video>` elements with the same src crossfade at the
|
|
86
|
-
cycle end; any seam disappears deterministically, no regeneration lottery
|
|
88
|
+
cycle end; any seam disappears deterministically, no regeneration lottery.
|
|
89
|
+
|
|
90
|
+
**Phone tiers get the poster, not the videos** (`$genex-threejs-adaptive-quality`):
|
|
91
|
+
two preloading 720p decoders while the 3D scene boots is a spike at exactly the
|
|
92
|
+
moment phones get killed for memory. On a phone tier, show the key-art poster
|
|
93
|
+
image (a captured frame of the clip works) and skip `seamlessLoop` entirely —
|
|
94
|
+
or defer ONE non-preloading video until after the first gameplay frame:
|
|
87
95
|
|
|
88
96
|
```ts
|
|
89
97
|
/** Deterministic seamless loop: two stacked <video>s crossfade at cycle end. */
|
|
@@ -203,6 +211,10 @@ copy-pasteable example:
|
|
|
203
211
|
|
|
204
212
|
```ts
|
|
205
213
|
// Buttons emit the SAME intents the gameplay input path uses — never a page reload.
|
|
214
|
+
// setPhase() carries the camera's lock lifecycle (game-ui's
|
|
215
|
+
// `followCam?.setPaused(phase !== "playing")` binding), so PLAY locks inside this
|
|
216
|
+
// click and Options/Credits clicks can never lock the pointer — a cursor that
|
|
217
|
+
// vanishes while a menu is still up is a shipped defect.
|
|
206
218
|
document.getElementById("menu-play")!.addEventListener("click", () => setPhase("playing"));
|
|
207
219
|
document.getElementById("menu-options")!.addEventListener("click", () => setPhase("options"));
|
|
208
220
|
document.getElementById("menu-credits")!.addEventListener("click", () => setPhase("credits"));
|
|
@@ -294,6 +306,10 @@ upgrades, in order of effort:
|
|
|
294
306
|
- **One menu = ONE video, generated one-off — not iteratively.** Video
|
|
295
307
|
generation is strictly rate-limited and takes minutes per attempt. Get the
|
|
296
308
|
still frame right first (images are cheap to redo), then animate it once.
|
|
309
|
+
The one exception: a user-driven style change (the game-ui concept loop
|
|
310
|
+
re-anchored the look) re-opens this rule once — re-edit the still
|
|
311
|
+
(`--edit` against the new concept frame) and re-run the video from the new
|
|
312
|
+
still. Agent-initiated polish never does.
|
|
297
313
|
- **720p is right.** It's a background under UI; higher resolutions cost more,
|
|
298
314
|
take longer, and change nothing visible.
|
|
299
315
|
- **Pause/victory/defeat variants reuse the same video — as GRADES.** Same
|
|
@@ -35,15 +35,23 @@ in local dev, the published game, and remixes).
|
|
|
35
35
|
|
|
36
36
|
## Load it as background + environment
|
|
37
37
|
|
|
38
|
-
Load the equirect JPG
|
|
39
|
-
background and the lighting
|
|
38
|
+
Load the equirect JPG through the quality tier's rung ladder, mark it
|
|
39
|
+
equirectangular, and use it for both the visible background and the lighting.
|
|
40
|
+
The bare URL is an 8192×4096 original — ~178 MB decoded, over half a phone's
|
|
41
|
+
GPU budget in one texture — so phones must load the downscale rung the platform
|
|
42
|
+
stores next to every skybox (`$genex-threejs-adaptive-quality`):
|
|
40
43
|
|
|
41
44
|
```ts
|
|
42
45
|
import * as THREE from "three";
|
|
46
|
+
import { detectTier } from "./controllers/quality/tier.ts";
|
|
47
|
+
import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
|
|
43
48
|
|
|
44
49
|
// the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
|
|
45
50
|
const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
|
|
46
|
-
const
|
|
51
|
+
const tier = detectTier(); // reuse the boot tier if you already have it
|
|
52
|
+
const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
|
|
53
|
+
new THREE.TextureLoader().loadAsync(u),
|
|
54
|
+
);
|
|
47
55
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
48
56
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
49
57
|
|
|
@@ -51,6 +59,9 @@ scene.background = texture; // visible sky
|
|
|
51
59
|
scene.environment = texture; // image-based lighting on PBR materials
|
|
52
60
|
```
|
|
53
61
|
|
|
62
|
+
Desktop gets the original; phones get the `@2048`/`@4096` rung; a missing rung
|
|
63
|
+
falls back to the original automatically — never a broken boot.
|
|
64
|
+
|
|
54
65
|
For sharper reflections/lighting, pre-filter it with `PMREMGenerator`:
|
|
55
66
|
|
|
56
67
|
```ts
|
|
@@ -76,7 +87,7 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
76
87
|
|
|
77
88
|
## Troubleshooting
|
|
78
89
|
|
|
79
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
90
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
80
91
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
81
92
|
this skybox generation. Tell the user the facts the CLI printed: their balance, this
|
|
82
93
|
generation's cost, and when their credits refill. Then offer to continue the build
|
|
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
|
|
|
225
225
|
|
|
226
226
|
## Troubleshooting
|
|
227
227
|
|
|
228
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
228
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
229
229
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
230
230
|
this texture generation. Tell the user the facts the CLI printed: their balance,
|
|
231
231
|
this generation's cost, and when their credits refill. Then offer to continue the
|
|
@@ -145,7 +145,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
|
|
|
145
145
|
|
|
146
146
|
## Troubleshooting
|
|
147
147
|
|
|
148
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
148
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
149
149
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
150
150
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
151
151
|
- **Nothing plays / black surface** — the first `video.play()` must run inside a user
|
|
@@ -71,7 +71,7 @@ Add `--json` for machine-readable output.
|
|
|
71
71
|
|
|
72
72
|
1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
|
|
73
73
|
2. `cd <name>`, then run init on the SAME CLI channel your current project
|
|
74
|
-
uses: `npx @genex-ai/cli-demo@
|
|
74
|
+
uses: `npx @genex-ai/cli-demo@dev init <name>` — or `@dev` when you're
|
|
75
75
|
on the dev stand (unsure? `dashboardOrigins` in your current project's
|
|
76
76
|
`.genex/project.json` says which). Never use `--force`. This creates your
|
|
77
77
|
own project; the original is untouched.
|
|
@@ -130,7 +130,7 @@ and re-link the clone to the same live game:
|
|
|
130
130
|
```bash
|
|
131
131
|
git clone <the game's repo url> my-game && cd my-game
|
|
132
132
|
npm install
|
|
133
|
-
npx @genex-ai/cli-demo@
|
|
133
|
+
npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Don't know the slug? **`npx genex list`** prints every game on your account —
|
|
@@ -157,7 +157,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
|
157
157
|
and your own files are never touched:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
npx @genex-ai/cli-demo@
|
|
160
|
+
npx @genex-ai/cli-demo@dev init
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
Use `--force` only if you intentionally want your own existing files overwritten
|