Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5c10d22
Add Cinder Bay and Floodlight Eleven kernel play slices.
cursoragent Aug 25, 2026
2efc7d2
Clone kernel clips per pawn so team clones actually animate.
cursoragent Aug 25, 2026
5794cb3
Fix kernel pawn crossfade so clone mixers are not blended to bind pose.
cursoragent Aug 25, 2026
b824a32
Parse a fresh kernel GLB per pawn so every clone has its own skeleton.
cursoragent Aug 25, 2026
43d8c27
Clone the kernel with SkeletonUtils so every pawn has its own mixer.
cursoragent Aug 25, 2026
f66f439
Put start CTAs above how-to copy so Cinder Bay is playable on short s…
cursoragent Aug 25, 2026
163484b
Allow the Next.js dev server to serve 127.0.0.1 origins.
cursoragent Aug 25, 2026
c7ee714
Latch on-screen WASD so a tap keeps jogging instead of a one-frame nu…
cursoragent Aug 25, 2026
99c290a
Pull the Cinder Bay chase camera farther back so jogging does not clip.
cursoragent Aug 25, 2026
3a9e694
Rebind cloned kernel meshes so football AI keeps cycling after the fi…
cursoragent Aug 25, 2026
042cf2d
Keep looping kernel clips from clamping at the last frame.
cursoragent Aug 25, 2026
cfda2d6
Do not rewind looping clips to t=0 every frame.
cursoragent Aug 25, 2026
b10b9d3
Parse a unique kernel GLB per pawn with the mixer on the scene root.
cursoragent Aug 25, 2026
016d72a
Temporarily copy the user skeleton onto football AI to test clone ski…
cursoragent Aug 25, 2026
a5e7f35
Drive each pawn mixer from the SkinnedMesh skeleton, not the scene root.
cursoragent Aug 25, 2026
57228dd
Remove play demos and stream live Meshy progress under game reverse.
cursoragent Aug 26, 2026
f05c1a1
Fall back from Meshy SSE to poll and always keep kernel prompt lines.
cursoragent Aug 26, 2026
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
8 changes: 5 additions & 3 deletions app/api/game-assets/[slug]/[file]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function corsHeaders(extra: Record<string, string> = {}): Record<string, string>
};
}

export async function GET(_request: NextRequest, context: RouteContext) {
export async function GET(request: NextRequest, context: RouteContext) {
const { slug: rawSlug, file: rawFile } = await context.params;
const slug = rawSlug.trim().toLowerCase();
const filename = rawFile.trim();
Expand All @@ -37,8 +37,10 @@ export async function GET(_request: NextRequest, context: RouteContext) {
status: 200,
headers: corsHeaders({
"Content-Type": "model/gltf-binary",
"Content-Disposition": `attachment; filename="${filename}"`,
"Cache-Control": "public, max-age=86400, s-maxage=86400",
"Content-Disposition": `inline; filename="${filename}"`,
"Cache-Control": request.nextUrl.searchParams.has("v")
? "private, no-store"
: "public, max-age=86400, s-maxage=86400",
}),
});
}
Expand Down
1 change: 1 addition & 0 deletions app/api/reverse-game/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async function executeGameReverse(opts: {
gameName,
force,
onStatus: (message) => send("status", { message }),
onHero: (hero) => send("hero", hero),
});

if (!result.ok) {
Expand Down
19 changes: 18 additions & 1 deletion components/game-reverse-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ 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 { MeshyLiveStage } from "@/components/meshy-live-stage";
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";
import {
mergeHeroProgress,
parseHeroProgressEvent,
type HeroProgressEvent,
} from "@/lib/meshy-progress";

type GameReversePageProps = {
gameSlug: string;
Expand All @@ -29,6 +35,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
const [heroAssets, setHeroAssets] = useState<
Array<StoredHeroAsset & { url: string }>
>([]);
const [liveHeroes, setLiveHeroes] = useState<HeroProgressEvent[]>([]);
const started = useRef(false);
const resultsRef = useRef<HTMLDivElement>(null);

Expand All @@ -53,6 +60,7 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
setError(null);
setPrompt(null);
setHeroAssets([]);
setLiveHeroes([]);
setStatusLine("Checking if it's cached…");

try {
Expand Down Expand Up @@ -116,11 +124,17 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
prompt?: string;
fromCache?: boolean;
error?: string;
};
} & Partial<HeroProgressEvent>;

if (event === "status" && typeof json.message === "string") {
setStatusLine(json.message);
}
if (event === "hero") {
const hero = parseHeroProgressEvent(json);
if (hero) {
setLiveHeroes((prev) => mergeHeroProgress(prev, hero));
}
}
if (event === "done" && typeof json.prompt === "string") {
setPrompt(json.prompt);
if (json.fromCache) setStatusLine("Loaded from cache");
Expand Down Expand Up @@ -303,6 +317,9 @@ export function GameReversePage({ gameSlug, gameName }: GameReversePageProps) {
) : null}
</form>
</div>
{loading && liveHeroes.length > 0 ? (
<MeshyLiveStage heroes={liveHeroes} />
) : null}
</div>

{prompt ? (
Expand Down
40 changes: 27 additions & 13 deletions components/hero-kernel-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ type HeroKernelPreviewProps = {
title?: string;
subtitle?: string;
autoClip?: QuaterniusClip;
compact?: boolean;
};

export function HeroKernelPreview({
modelUrl,
title = "Quaternius kernel",
subtitle,
autoClip = "Idle_Loop",
compact = false,
}: HeroKernelPreviewProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [clip, setClip] = useState<string>(autoClip);
Expand Down Expand Up @@ -109,8 +111,14 @@ export function HeroKernelPreview({
if (disposed) return;

const root = gltf.scene;
let skin: import("three").SkinnedMesh | null = null;
root.traverse((obj) => {
const mesh = obj as import("three").Mesh;
const mesh = obj as import("three").SkinnedMesh;
if (mesh.isSkinnedMesh) {
mesh.bind(mesh.skeleton, mesh.bindMatrix);
mesh.frustumCulled = false;
if (!skin) skin = mesh;
}
if (mesh.isMesh) {
mesh.castShadow = true;
mesh.receiveShadow = true;
Expand All @@ -122,10 +130,10 @@ export function HeroKernelPreview({
const names = clips.map((c) => c.name).filter(Boolean);
setAvailable(names.length ? names : [...KERNEL_PREVIEW_CLIPS]);

mixer = new THREE.AnimationMixer(root);
mixer = new THREE.AnimationMixer(skin ?? root);
const actions = new Map<string, import("three").AnimationAction>();
for (const c of clips) {
const action = mixer.clipAction(c);
const action = mixer.clipAction(c.clone());
action.enabled = true;
actions.set(c.name, action);
}
Expand Down Expand Up @@ -198,26 +206,32 @@ export function HeroKernelPreview({

return (
<div className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-zinc-700">{title}</h3>
<p className="mt-0.5 text-xs text-zinc-500">
{subtitle ?? "Quaternius Universal Animation Library kernel"}
{compact ? null : (
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-zinc-700">{title}</h3>
<p className="mt-0.5 text-xs text-zinc-500">
{subtitle ?? "Quaternius Universal Animation Library kernel"}
</p>
</div>
<p className="text-xs font-medium text-zinc-500" role="status">
{error ? error : status}
</p>
</div>
<p className="text-xs font-medium text-zinc-500" role="status">
{error ? error : status}
</p>
</div>
)}
<div className="relative overflow-hidden rounded-lg border-[3px] border-zinc-900 bg-[#f6efe2]">
<canvas
ref={canvasRef}
className="block h-[22rem] w-full"
className={`block w-full ${compact ? "h-[16rem]" : "h-[22rem]"}`}
aria-label="Animated hero preview"
/>
</div>
{error ? (
<p className="text-sm text-red-700">{error}</p>
) : compact ? (
<p className="text-xs font-medium text-zinc-500" role="status">
{status}
</p>
) : (
<div className="flex flex-wrap gap-1.5">
{clipButtons.map((name) => (
Expand Down
69 changes: 69 additions & 0 deletions components/meshy-live-stage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { HeroKernelPreview } from "@/components/hero-kernel-preview";
import type { HeroProgressEvent } from "@/lib/meshy-progress";

function stageLabel(hero: HeroProgressEvent): string {
if (hero.stage === "ready") {
return hero.kernel ? "Walking" : "Ready";
}
if (hero.stage === "kernel") return "Auto-rig";
if (hero.stage === "refine") return `Texture ${hero.progress}%`;
if (hero.stage === "failed") return "Failed";
return `Sculpt ${hero.progress}%`;
}

function LiveHeroCard({ hero }: { hero: HeroProgressEvent }) {
const moving = Boolean(hero.kernel && hero.modelUrl);
return (
<div className="relative">
<div className="absolute inset-0 translate-x-2 translate-y-2 rounded-xl bg-zinc-900" />
<section className="relative z-10 overflow-hidden rounded-xl border-[3px] border-zinc-900 bg-[#fafafa] p-4">
<div className="mb-2 flex items-baseline justify-between gap-3">
<p className="text-sm font-semibold text-zinc-800">{hero.id}</p>
<p className="text-xs font-medium text-zinc-500" role="status">
{stageLabel(hero)}
</p>
</div>
<div className="mb-3 h-1.5 overflow-hidden rounded-full bg-zinc-200">
<div
className="h-full bg-zinc-900 transition-[width] duration-500"
style={{ width: `${Math.max(hero.stage === "ready" ? 100 : hero.progress, 2)}%` }}
/>
</div>
{hero.modelUrl ? (
<HeroKernelPreview
key={hero.modelUrl}
modelUrl={hero.modelUrl}
autoClip={moving ? "Walk_Loop" : "Idle_Loop"}
compact
/>
) : hero.thumbnailUrl ? (
<div className="overflow-hidden rounded-lg border-[3px] border-zinc-900 bg-[#f6efe2]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={hero.thumbnailUrl}
alt=""
className="mx-auto h-[16rem] w-auto object-contain"
/>
</div>
) : (
<div className="flex h-24 items-center justify-center rounded-lg border-[3px] border-dashed border-zinc-300 bg-white text-xs text-zinc-500">
{hero.status}
</div>
)}
</section>
</div>
);
}

export function MeshyLiveStage({ heroes }: { heroes: HeroProgressEvent[] }) {
if (!heroes.length) return null;
return (
<div className="flex w-full max-w-2xl flex-col gap-3" aria-live="polite">
{heroes.map((hero) => (
<LiveHeroCard key={hero.id} hero={hero} />
))}
</div>
);
}
Loading