game-engine.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. "use client";
  2. import { useMemo, useEffect, Suspense, useState } from "react";
  3. import { Canvas } from "@react-three/fiber";
  4. import { Physics } from "@react-three/rapier";
  5. import { ThreeRenderer } from "@json-render/react-three-fiber";
  6. import { Braces, SlidersHorizontal, X } from "lucide-react";
  7. import { useEditorStore } from "@/lib/store";
  8. import { useIsMobile } from "@/lib/use-mobile";
  9. import { registry } from "@/lib/registry";
  10. import { sceneToSpec } from "@/lib/scene-to-spec";
  11. import { Toolbar } from "@/components/editor/toolbar";
  12. import { SceneInspector } from "@/components/editor/scene-inspector";
  13. import { AddObjectButton } from "@/components/editor/add-object-button";
  14. import { EditorControls } from "@/components/editor/transform-controls";
  15. import { JsonPane } from "@/components/editor/json-pane";
  16. import { AIPrompt } from "@/components/editor/ai-prompt";
  17. import { HealthBar } from "@/components/hud/health-bar";
  18. import { DamageEffect } from "@/components/hud/damage-effect";
  19. import { GameOverScreen } from "@/components/hud/game-over";
  20. import { CharacterDialog } from "@/components/hud/character-dialog";
  21. import { InteractionPrompt } from "@/components/hud/interaction-prompt";
  22. import { InGamePrompt } from "@/components/hud/in-game-prompt";
  23. import { DamageSound } from "@/components/hud/damage-sound";
  24. import { DeathSound } from "@/components/hud/death-sound";
  25. import { LoadingSpinner } from "@/components/hud/loading-spinner";
  26. import { TouchControls } from "@/components/hud/touch-controls";
  27. import { CharacterInteraction } from "@/components/game/character-interaction";
  28. import { DropZone } from "@/components/editor/drop-zone";
  29. export function GameEngine() {
  30. const scenes = useEditorStore((s) => s.scenes);
  31. const activeSceneId = useEditorStore((s) => s.activeSceneId);
  32. const isPlaying = useEditorStore((s) => s.isPlaying);
  33. const setIsPlaying = useEditorStore((s) => s.setIsPlaying);
  34. const selectedObjectId = useEditorStore((s) => s.selectedObjectId);
  35. const health = useEditorStore((s) => s.health);
  36. const lastDamageTime = useEditorStore((s) => s.lastDamageTime);
  37. const undo = useEditorStore((s) => s.undo);
  38. const redo = useEditorStore((s) => s.redo);
  39. const removeObject = useEditorStore((s) => s.removeObject);
  40. const isMobile = useIsMobile();
  41. const [leftDrawer, setLeftDrawer] = useState(false);
  42. const [rightDrawer, setRightDrawer] = useState(false);
  43. const activeScene = useMemo(
  44. () => scenes.find((s) => s.id === activeSceneId) || scenes[0],
  45. [scenes, activeSceneId],
  46. );
  47. const spec = useMemo(() => {
  48. if (!activeScene) return null;
  49. return sceneToSpec(activeScene);
  50. }, [activeScene]);
  51. useEffect(() => {
  52. const handleKeyDown = (e: KeyboardEvent) => {
  53. const isMod = e.metaKey || e.ctrlKey;
  54. if (isMod && e.key === "Enter") {
  55. e.preventDefault();
  56. setIsPlaying(!isPlaying);
  57. return;
  58. }
  59. if (isMod && (e.key === "Backspace" || e.key === "Delete")) {
  60. e.preventDefault();
  61. if (selectedObjectId && !isPlaying) {
  62. removeObject(selectedObjectId);
  63. }
  64. return;
  65. }
  66. if (isMod && e.key === "z" && !e.shiftKey) {
  67. e.preventDefault();
  68. undo();
  69. return;
  70. }
  71. if (isMod && (e.key === "y" || (e.key === "z" && e.shiftKey))) {
  72. e.preventDefault();
  73. redo();
  74. return;
  75. }
  76. };
  77. window.addEventListener("keydown", handleKeyDown);
  78. return () => window.removeEventListener("keydown", handleKeyDown);
  79. }, [isPlaying, setIsPlaying, selectedObjectId, removeObject, undo, redo]);
  80. useEffect(() => {
  81. if (isPlaying) {
  82. setLeftDrawer(false);
  83. setRightDrawer(false);
  84. }
  85. }, [isPlaying]);
  86. const showSidebars = !isPlaying;
  87. return (
  88. <div className="h-dvh w-screen flex flex-col bg-[#0a0a0a] overflow-hidden">
  89. <Toolbar />
  90. <div className="flex flex-1 min-h-0 relative">
  91. {/* Left sidebar - JSON Pane (desktop only) */}
  92. {showSidebars && (
  93. <div className="hidden sm:block w-72 shrink-0 border-r border-[#1e1e1e] bg-[#0f0f0f] overflow-hidden">
  94. <JsonPane />
  95. </div>
  96. )}
  97. {/* Mobile left drawer */}
  98. {showSidebars && (
  99. <>
  100. <div
  101. className={`fixed inset-0 bg-black/50 z-30 transition-opacity duration-200 ${leftDrawer ? "opacity-100" : "opacity-0 pointer-events-none"}`}
  102. onClick={() => setLeftDrawer(false)}
  103. />
  104. <div
  105. className={`fixed left-0 top-0 bottom-0 w-72 max-w-[85vw] bg-[#0f0f0f] border-r border-[#1e1e1e] z-40 overflow-hidden transition-transform duration-200 ease-out ${leftDrawer ? "translate-x-0" : "-translate-x-full"}`}
  106. style={{ paddingTop: "env(safe-area-inset-top, 0px)" }}
  107. >
  108. <div className="flex items-center justify-between px-3 py-2 border-b border-[#1e1e1e]">
  109. <span className="text-[10px] font-semibold text-[#666] uppercase tracking-wider">
  110. JSON Spec
  111. </span>
  112. <button
  113. onClick={() => setLeftDrawer(false)}
  114. className="p-1.5 text-[#666] hover:text-white active:text-white"
  115. >
  116. <X size={14} />
  117. </button>
  118. </div>
  119. <JsonPane />
  120. </div>
  121. </>
  122. )}
  123. {/* Canvas area */}
  124. <div className="flex-1 relative min-w-0 safe-bottom">
  125. <DropZone />
  126. <Canvas
  127. shadows
  128. camera={{ position: [5, 5, 5], fov: 50 }}
  129. style={{ width: "100%", height: "100%", touchAction: "none" }}
  130. >
  131. <Suspense fallback={null}>
  132. {isPlaying ? (
  133. <Physics gravity={[0, -9.81, 0]}>
  134. <ThreeRenderer spec={spec} registry={registry} />
  135. <CharacterInteraction />
  136. </Physics>
  137. ) : (
  138. <>
  139. <ThreeRenderer spec={spec} registry={registry} />
  140. <EditorControls />
  141. </>
  142. )}
  143. </Suspense>
  144. </Canvas>
  145. {/* Mobile drawer toggle buttons */}
  146. {showSidebars && (
  147. <>
  148. <button
  149. onClick={() => setLeftDrawer(true)}
  150. className="sm:hidden absolute top-2 left-2 z-10 w-11 h-11 flex items-center justify-center rounded-full bg-white/10 active:bg-white/30 text-white transition-colors backdrop-blur-sm"
  151. >
  152. <Braces size={18} />
  153. </button>
  154. <button
  155. onClick={() => setRightDrawer(true)}
  156. className="sm:hidden absolute top-2 right-2 z-10 w-11 h-11 flex items-center justify-center rounded-full bg-white/10 active:bg-white/30 text-white transition-colors backdrop-blur-sm"
  157. >
  158. <SlidersHorizontal size={18} />
  159. </button>
  160. </>
  161. )}
  162. {!isPlaying && <AddObjectButton />}
  163. {!isPlaying && <AIPrompt />}
  164. <LoadingSpinner />
  165. {isPlaying && <HealthBar />}
  166. {isPlaying && <DamageEffect lastDamageTime={lastDamageTime} />}
  167. {isPlaying && health <= 0 && <GameOverScreen />}
  168. {isPlaying && <CharacterDialog />}
  169. {isPlaying && <InteractionPrompt />}
  170. {isPlaying && <InGamePrompt />}
  171. {isPlaying && isMobile && <TouchControls />}
  172. <DamageSound />
  173. <DeathSound />
  174. </div>
  175. {/* Right sidebar - Inspector (desktop only) */}
  176. {showSidebars && (
  177. <div className="hidden sm:block w-72 shrink-0 border-l border-[#1e1e1e] bg-[#0f0f0f] overflow-y-auto">
  178. <SceneInspector />
  179. </div>
  180. )}
  181. {/* Mobile right drawer */}
  182. {showSidebars && (
  183. <>
  184. <div
  185. className={`fixed inset-0 bg-black/50 z-30 transition-opacity duration-200 ${rightDrawer ? "opacity-100" : "opacity-0 pointer-events-none"}`}
  186. onClick={() => setRightDrawer(false)}
  187. />
  188. <div
  189. className={`fixed right-0 top-0 bottom-0 w-72 max-w-[85vw] bg-[#0f0f0f] border-l border-[#1e1e1e] z-40 overflow-y-auto transition-transform duration-200 ease-out ${rightDrawer ? "translate-x-0" : "translate-x-full"}`}
  190. style={{ paddingTop: "env(safe-area-inset-top, 0px)" }}
  191. >
  192. <div className="flex items-center justify-between px-3 py-2 border-b border-[#1e1e1e]">
  193. <span className="text-[10px] font-semibold text-[#666] uppercase tracking-wider">
  194. Inspector
  195. </span>
  196. <button
  197. onClick={() => setRightDrawer(false)}
  198. className="p-1.5 text-[#666] hover:text-white active:text-white"
  199. >
  200. <X size={14} />
  201. </button>
  202. </div>
  203. <SceneInspector />
  204. </div>
  205. </>
  206. )}
  207. </div>
  208. </div>
  209. );
  210. }