Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ VIEWS_IP_SALT=
# context.dev fallback when Firecrawl is blocked (get key at https://context.dev)
# CONTEXT_DEV_API_KEY=

# --- Game reverse 3D hero meshes (Meshy text-to-3D + optional auto-rig) ---
# --- Game reverse 3D hero meshes (Meshy text-to-3D + Quaternius kernel auto-rig) ---
# MESHY_API_KEY=msy_...
# After Meshy sculpts a humanoid, gitreverse auto-rigs it onto the vendored
# Quaternius Universal Animation Library skeleton (public/quaternius/) and
# embeds Idle_Loop / Walk_Loop / Sprint_Loop / etc. Preview: /game/kernel

# --- Stripe checkout (optional: Premium subscription and pay-per-use credits) ---
# Server-side Checkout Session — success URL is set in code (?session_id={CHECKOUT_SESSION_ID}).
Expand Down
42 changes: 42 additions & 0 deletions app/api/game-kernel/fixture/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import { autoRigToQuaterniusKernel } from "@/lib/auto-rig-humanoid";
import { buildTPoseDummyGlb } from "@/lib/tpose-dummy";

export const runtime = "nodejs";

function corsHeaders(extra: Record<string, string> = {}): Record<string, string> {
return {
"Access-Control-Allow-Origin": "*",
...extra,
};
}

export async function GET() {
const dummy = await buildTPoseDummyGlb();
const rigged = await autoRigToQuaterniusKernel(dummy);
if (!rigged.ok) {
return NextResponse.json(
{ error: rigged.error },
{ status: 500, headers: corsHeaders() }
);
}

return new NextResponse(new Uint8Array(rigged.glb), {
status: 200,
headers: corsHeaders({
"Content-Type": "model/gltf-binary",
"Content-Disposition": 'inline; filename="kernel-fixture.glb"',
"Cache-Control": "public, max-age=60",
}),
});
}

export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: corsHeaders({
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}),
});
}
11 changes: 11 additions & 0 deletions app/game/kernel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Metadata } from "next";
import { KernelLabPage } from "@/components/kernel-lab-page";

export const metadata: Metadata = {
title: "Quaternius movement kernel",
robots: { index: false, follow: false },
};

export default function Page() {
return <KernelLabPage />;
}
22 changes: 21 additions & 1 deletion app/specs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { notFound } from "next/navigation";
import { Navbar } from "@/components/navbar";
import { PromptMarkdown } from "@/components/prompt-markdown";
import { HeroKernelPreview } from "@/components/hero-kernel-preview";
import { isValidGameSlug } from "@/lib/parse-game-input";
import { readGameReverse } from "@/lib/game-reverse-storage";
import { readHeroAssetManifest } from "@/lib/game-asset-storage";
Expand All @@ -24,6 +25,7 @@ export default async function GameSpecPage({ params }: PageProps) {

const downloadHref = `/api/game-spec/${encodeURIComponent(slug)}?download=1`;
const heroAssets = (await readHeroAssetManifest(slug))?.assets ?? [];
const heroPreview = heroAssets.find((asset) => asset.kind === "humanoid");

return (
<div className="flex min-h-screen flex-col bg-[#FFFDF8] text-zinc-900">
Expand Down Expand Up @@ -58,6 +60,20 @@ export default async function GameSpecPage({ params }: PageProps) {
<div className="overflow-auto rounded-lg border border-zinc-200 bg-white p-4 text-sm leading-relaxed text-zinc-800">
<PromptMarkdown>{cached.specMd}</PromptMarkdown>
</div>
{heroPreview ? (
<div className="mt-4 border-t border-zinc-200 pt-4">
<HeroKernelPreview
modelUrl={`/api/game-assets/${encodeURIComponent(slug)}/${encodeURIComponent(heroPreview.filename)}`}
title="Generated hero"
subtitle={
heroPreview.kernel
? "Auto-rigged to the Quaternius Universal skeleton"
: "Textured sculpt"
}
autoClip="Walk_Loop"
/>
</div>
) : null}
{heroAssets.length > 0 ? (
<div className="mt-4 border-t border-zinc-200 pt-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
Expand All @@ -74,7 +90,11 @@ export default async function GameSpecPage({ params }: PageProps) {
Download {asset.filename}
</a>
<span className="ml-2 text-xs text-zinc-500">
{asset.hasWalk ? "rigged walk" : "textured sculpt"}
{asset.kernel
? "Quaternius kernel"
: asset.hasWalk
? "rigged walk"
: "textured sculpt"}
</span>
</li>
))}
Expand Down
55 changes: 54 additions & 1 deletion components/game-reverse-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { GameSpecFlavorText } from "@/components/game-spec-flavor-text";
import { HeroKernelPreview } from "@/components/hero-kernel-preview";
import { Navbar } from "@/components/navbar";
import { PromptMarkdown } from "@/components/prompt-markdown";
import { nameToSlug, parseGameInput } from "@/lib/parse-game-input";
import type { StoredHeroAsset } from "@/lib/game-asset-storage";

type GameReversePageProps = {
gameSlug: string;
Expand All @@ -24,13 +26,33 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
const [loading, setLoading] = useState(true);
const [statusLine, setStatusLine] = useState("Checking if it's cached…");
const [copied, setCopied] = useState(false);
const [heroAssets, setHeroAssets] = useState<
Array<StoredHeroAsset & { url: string }>
>([]);
const started = useRef(false);
const resultsRef = useRef<HTMLDivElement>(null);

const loadHeroAssets = useCallback(async (slug: string) => {
try {
const res = await fetch(`/api/game-assets/${encodeURIComponent(slug)}`);
if (!res.ok) {
setHeroAssets([]);
return;
}
const data = (await res.json()) as {
assets?: Array<StoredHeroAsset & { url: string }>;
};
setHeroAssets(data.assets ?? []);
} catch {
setHeroAssets([]);
}
}, []);

const run = useCallback(async (slug: string, name: string) => {
setLoading(true);
setError(null);
setPrompt(null);
setHeroAssets([]);
setStatusLine("Checking if it's cached…");

try {
Expand All @@ -57,6 +79,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
if (data.prompt) {
setPrompt(data.prompt);
if (data.fromCache) setStatusLine("Loaded from cache");
void loadHeroAssets(slug);
} else {
throw new Error("No prompt returned.");
}
Expand Down Expand Up @@ -101,6 +124,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
if (event === "done" && typeof json.prompt === "string") {
setPrompt(json.prompt);
if (json.fromCache) setStatusLine("Loaded from cache");
void loadHeroAssets(slug);
}
if (event === "error" && typeof json.error === "string") {
throw new Error(json.error);
Expand All @@ -117,7 +141,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
} finally {
setLoading(false);
}
}, []);
}, [loadHeroAssets]);

useEffect(() => {
if (started.current) return;
Expand Down Expand Up @@ -318,6 +342,35 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
</section>
</div>
) : null}

{prompt ? (
<div className="relative w-full max-w-2xl">
<div className="absolute inset-0 translate-x-2 translate-y-2 rounded-xl bg-zinc-900" />
<section className="relative z-10 rounded-xl border-[3px] border-zinc-900 bg-[#fafafa] p-6">
{heroAssets.find((a) => a.kind === "humanoid") ? (
<HeroKernelPreview
modelUrl={
heroAssets.find((a) => a.kind === "humanoid")?.url ??
undefined
}
title="Generated hero"
subtitle={
heroAssets.find((a) => a.kernel)
? "Auto-rigged to the Quaternius Universal skeleton"
: "Textured sculpt (kernel bind pending)"
}
autoClip="Walk_Loop"
/>
) : (
<HeroKernelPreview
title="Movement kernel"
subtitle="Generated heroes are auto-rigged onto this Quaternius Universal rig and play these clips"
autoClip="Walk_Loop"
/>
)}
</section>
</div>
) : null}
</main>
</div>
);
Expand Down
Loading