store.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import { create } from "zustand";
  2. import { v4 as uuidv4 } from "uuid";
  3. import type {
  4. EditorState,
  5. Scene,
  6. SceneObject,
  7. ObjectType,
  8. TransformMode,
  9. ViewMode,
  10. } from "./types";
  11. import {
  12. createDefaultScene,
  13. createDefaultObject,
  14. createDefaultGrass,
  15. createDefaultAmbientLight,
  16. createDefaultPlayer,
  17. createDefaultSceneSettings,
  18. createDefaultEnvironmentSettings,
  19. } from "./defaults";
  20. function saveHistory(state: EditorState): Partial<EditorState> {
  21. return {
  22. history: {
  23. past: [...state.history.past.slice(-49), state.scenes],
  24. future: [],
  25. },
  26. canUndo: true,
  27. canRedo: false,
  28. };
  29. }
  30. const initialScene = createDefaultScene();
  31. export const useEditorStore = create<EditorState>((set, get) => ({
  32. scenes: [initialScene],
  33. activeSceneId: initialScene.id,
  34. selectedObjectId: null,
  35. transformMode: "translate" as TransformMode,
  36. isPlaying: false,
  37. isLoading: false,
  38. viewMode: "first-person" as ViewMode,
  39. isPromptOpen: false,
  40. health: 100,
  41. shield: 50,
  42. maxHealth: 100,
  43. maxShield: 100,
  44. lastDamageTime: null,
  45. damageCooldown: 0.5,
  46. history: { past: [], future: [] },
  47. canUndo: false,
  48. canRedo: false,
  49. get activeScene() {
  50. const state = get();
  51. return (
  52. state.scenes.find((s) => s.id === state.activeSceneId) || state.scenes[0]
  53. );
  54. },
  55. get activeSceneObjects() {
  56. const state = get();
  57. const scene =
  58. state.scenes.find((s) => s.id === state.activeSceneId) || state.scenes[0];
  59. return scene ? scene.objects : [];
  60. },
  61. // Scene actions
  62. createScene: (name = "New Scene") =>
  63. set((state) => {
  64. const newSceneId = uuidv4();
  65. const newScene: Scene = {
  66. id: newSceneId,
  67. name,
  68. description: "",
  69. isDefault: state.scenes.length === 0,
  70. objects: [
  71. createDefaultGrass(),
  72. createDefaultAmbientLight(),
  73. createDefaultPlayer(),
  74. ],
  75. sceneSettings: createDefaultSceneSettings(),
  76. environmentSettings: createDefaultEnvironmentSettings(),
  77. };
  78. return {
  79. scenes: [...state.scenes, newScene],
  80. activeSceneId: newSceneId,
  81. selectedObjectId: null,
  82. };
  83. }),
  84. deleteScene: (id) =>
  85. set((state) => {
  86. if (state.scenes.length <= 1) return state;
  87. const updated = state.scenes.filter((s) => s.id !== id);
  88. const deletedWasDefault = state.scenes.find(
  89. (s) => s.id === id,
  90. )?.isDefault;
  91. if (deletedWasDefault && updated[0]) updated[0].isDefault = true;
  92. return {
  93. scenes: updated,
  94. activeSceneId:
  95. state.activeSceneId === id ? updated[0]!.id : state.activeSceneId,
  96. selectedObjectId: null,
  97. };
  98. }),
  99. duplicateScene: (id) =>
  100. set((state) => {
  101. const src = state.scenes.find((s) => s.id === id);
  102. if (!src) return state;
  103. const newId = uuidv4();
  104. const clone: Scene = {
  105. ...JSON.parse(JSON.stringify(src)),
  106. id: newId,
  107. name: `${src.name} (Copy)`,
  108. isDefault: false,
  109. };
  110. return {
  111. scenes: [...state.scenes, clone],
  112. activeSceneId: newId,
  113. selectedObjectId: null,
  114. };
  115. }),
  116. setActiveScene: (id) =>
  117. set((state) => {
  118. if (!state.scenes.some((s) => s.id === id)) return state;
  119. return { activeSceneId: id, selectedObjectId: null };
  120. }),
  121. updateSceneName: (id, name) =>
  122. set((state) => ({
  123. scenes: state.scenes.map((s) => (s.id === id ? { ...s, name } : s)),
  124. })),
  125. updateSceneDescription: (id, description) =>
  126. set((state) => ({
  127. scenes: state.scenes.map((s) =>
  128. s.id === id ? { ...s, description } : s,
  129. ),
  130. })),
  131. // Object actions
  132. addObject: (type: ObjectType) =>
  133. set((state) => {
  134. const scene = state.scenes.find((s) => s.id === state.activeSceneId);
  135. if (!scene) return state;
  136. if (type === "player" && scene.objects.some((o) => o.type === "player"))
  137. return state;
  138. const id = uuidv4();
  139. const newObj = createDefaultObject(type, id, scene.objects.length);
  140. if (type === "light") {
  141. const lightCount = scene.objects.filter(
  142. (o) => o.type === "light",
  143. ).length;
  144. if (lightCount === 0) {
  145. newObj.name = "Ambient Light";
  146. newObj.intensity = 0.5;
  147. newObj.position = [0, 3, 0];
  148. newObj.lightType = "ambient";
  149. } else if (lightCount === 1) {
  150. newObj.name = "Directional Light";
  151. newObj.intensity = 1;
  152. newObj.position = [10, 10, 10];
  153. newObj.lightType = "directional";
  154. } else {
  155. newObj.name = "Point Light";
  156. newObj.intensity = 1;
  157. newObj.distance = 10;
  158. newObj.lightType = "point";
  159. }
  160. }
  161. const updatedScenes = state.scenes.map((s) =>
  162. s.id === state.activeSceneId
  163. ? { ...s, objects: [...s.objects, newObj] }
  164. : s,
  165. );
  166. return {
  167. ...saveHistory({ ...state, scenes: updatedScenes }),
  168. scenes: updatedScenes,
  169. selectedObjectId: id,
  170. };
  171. }),
  172. createCustomObject: (type: ObjectType, properties: Partial<SceneObject>) =>
  173. set((state) => {
  174. const scene = state.scenes.find((s) => s.id === state.activeSceneId);
  175. if (!scene) return state;
  176. if (type === "player" && scene.objects.some((o) => o.type === "player"))
  177. return state;
  178. const id = uuidv4();
  179. const base = createDefaultObject(type, id, scene.objects.length);
  180. const newObj: SceneObject = {
  181. ...base,
  182. ...properties,
  183. id,
  184. type,
  185. material: { ...base.material, ...(properties.material || {}) },
  186. physics: { ...base.physics, ...(properties.physics || {}) },
  187. };
  188. if (type === "light" && properties.lightType) {
  189. newObj.lightType = properties.lightType;
  190. if (
  191. properties.lightType === "ambient" &&
  192. properties.intensity === undefined
  193. )
  194. newObj.intensity = 0.5;
  195. if (properties.lightType === "spot" && properties.angle === undefined)
  196. newObj.angle = Math.PI / 3;
  197. if (
  198. properties.lightType === "spot" &&
  199. properties.penumbra === undefined
  200. )
  201. newObj.penumbra = 0;
  202. }
  203. if (type === "plane") {
  204. newObj.scale[2] = 1;
  205. if (!properties.rotation) newObj.rotation = [-Math.PI / 2, 0, 0];
  206. }
  207. const updatedScenes = state.scenes.map((s) =>
  208. s.id === state.activeSceneId
  209. ? { ...s, objects: [...s.objects, newObj] }
  210. : s,
  211. );
  212. return {
  213. ...saveHistory({ ...state, scenes: updatedScenes }),
  214. scenes: updatedScenes,
  215. selectedObjectId: id,
  216. };
  217. }),
  218. removeObject: (id) =>
  219. set((state) => {
  220. const updatedScenes = state.scenes.map((s) =>
  221. s.id === state.activeSceneId
  222. ? { ...s, objects: s.objects.filter((o) => o.id !== id) }
  223. : s,
  224. );
  225. return {
  226. ...saveHistory({ ...state, scenes: updatedScenes }),
  227. scenes: updatedScenes,
  228. selectedObjectId:
  229. state.selectedObjectId === id ? null : state.selectedObjectId,
  230. };
  231. }),
  232. replaceSceneObjects: (objects: SceneObject[]) =>
  233. set((state) => {
  234. const updatedScenes = state.scenes.map((s) =>
  235. s.id === state.activeSceneId ? { ...s, objects } : s,
  236. );
  237. const selectionStillExists =
  238. state.selectedObjectId != null &&
  239. objects.some((o) => o.id === state.selectedObjectId);
  240. return {
  241. ...saveHistory({ ...state, scenes: updatedScenes }),
  242. scenes: updatedScenes,
  243. selectedObjectId: selectionStillExists ? state.selectedObjectId : null,
  244. };
  245. }),
  246. initializePlayer: () =>
  247. set((state) => {
  248. const scene = state.scenes.find((s) => s.id === state.activeSceneId);
  249. if (!scene || scene.objects.some((o) => o.type === "player"))
  250. return state;
  251. const player = createDefaultPlayer();
  252. const updatedScenes = state.scenes.map((s) =>
  253. s.id === state.activeSceneId
  254. ? { ...s, objects: [...s.objects, player] }
  255. : s,
  256. );
  257. return { scenes: updatedScenes };
  258. }),
  259. // Updates
  260. selectObject: (id) => set({ selectedObjectId: id }),
  261. updateObjectTransform: (id, transform) =>
  262. set((state) => {
  263. const updatedScenes = state.scenes.map((s) => {
  264. if (s.id !== state.activeSceneId) return s;
  265. return {
  266. ...s,
  267. objects: s.objects.map((o) => {
  268. if (o.id !== id) return o;
  269. const t = { ...transform };
  270. if (o.type === "plane" && t.scale)
  271. (t.scale as [number, number, number])[2] = 1;
  272. return { ...o, ...t };
  273. }),
  274. };
  275. });
  276. return { scenes: updatedScenes };
  277. }),
  278. updateObjectMaterial: (id, material) =>
  279. set((state) => {
  280. const updatedScenes = state.scenes.map((s) => {
  281. if (s.id !== state.activeSceneId) return s;
  282. return {
  283. ...s,
  284. objects: s.objects.map((o) =>
  285. o.id === id
  286. ? { ...o, material: { ...o.material, ...material } }
  287. : o,
  288. ),
  289. };
  290. });
  291. return {
  292. ...saveHistory({ ...state, scenes: updatedScenes }),
  293. scenes: updatedScenes,
  294. };
  295. }),
  296. updateObjectPhysics: (id, physics) =>
  297. set((state) => {
  298. const updatedScenes = state.scenes.map((s) => {
  299. if (s.id !== state.activeSceneId) return s;
  300. return {
  301. ...s,
  302. objects: s.objects.map((o) =>
  303. o.id === id ? { ...o, physics: { ...o.physics, ...physics } } : o,
  304. ),
  305. };
  306. });
  307. return {
  308. ...saveHistory({ ...state, scenes: updatedScenes }),
  309. scenes: updatedScenes,
  310. };
  311. }),
  312. updateCharacter: (id, character) =>
  313. set((state) => {
  314. const updatedScenes = state.scenes.map((s) => {
  315. if (s.id !== state.activeSceneId) return s;
  316. return {
  317. ...s,
  318. objects: s.objects.map((o) =>
  319. o.id === id
  320. ? {
  321. ...o,
  322. character: { ...(o.character || { role: "" }), ...character },
  323. }
  324. : o,
  325. ),
  326. };
  327. });
  328. return {
  329. ...saveHistory({ ...state, scenes: updatedScenes }),
  330. scenes: updatedScenes,
  331. };
  332. }),
  333. updateSound: (id, soundUpdates) =>
  334. set((state) => {
  335. const updatedScenes = state.scenes.map((s) => {
  336. if (s.id !== state.activeSceneId) return s;
  337. return {
  338. ...s,
  339. objects: s.objects.map((o) => {
  340. if (o.id !== id) return o;
  341. const current = o.sound || {
  342. url: "",
  343. loop: false,
  344. volume: 1,
  345. positional: true,
  346. distance: 10,
  347. };
  348. return { ...o, sound: { ...current, ...soundUpdates } };
  349. }),
  350. };
  351. });
  352. return {
  353. ...saveHistory({ ...state, scenes: updatedScenes }),
  354. scenes: updatedScenes,
  355. };
  356. }),
  357. updateMedia: (id, mediaUpdates) =>
  358. set((state) => {
  359. const updatedScenes = state.scenes.map((s) => {
  360. if (s.id !== state.activeSceneId) return s;
  361. return {
  362. ...s,
  363. objects: s.objects.map((o) => {
  364. if (o.id !== id) return o;
  365. const current = o.media || {
  366. url: "",
  367. type: "image",
  368. loop: false,
  369. autoplay: false,
  370. muted: true,
  371. };
  372. return { ...o, media: { ...current, ...mediaUpdates } };
  373. }),
  374. };
  375. });
  376. return {
  377. ...saveHistory({ ...state, scenes: updatedScenes }),
  378. scenes: updatedScenes,
  379. };
  380. }),
  381. updateDamage: (id, damageUpdates) =>
  382. set((state) => {
  383. const updatedScenes = state.scenes.map((s) => {
  384. if (s.id !== state.activeSceneId) return s;
  385. return {
  386. ...s,
  387. objects: s.objects.map((o) => {
  388. if (o.id !== id) return o;
  389. const current = o.damage || { amount: 0, enabled: false };
  390. return { ...o, damage: { ...current, ...damageUpdates } };
  391. }),
  392. };
  393. });
  394. return {
  395. ...saveHistory({ ...state, scenes: updatedScenes }),
  396. scenes: updatedScenes,
  397. };
  398. }),
  399. updateSceneSettings: (settings) =>
  400. set((state) => {
  401. const updatedScenes = state.scenes.map((s) => {
  402. if (s.id !== state.activeSceneId) return s;
  403. return {
  404. ...s,
  405. sceneSettings: {
  406. ...s.sceneSettings,
  407. ...settings,
  408. grid: { ...s.sceneSettings.grid, ...(settings.grid || {}) },
  409. fog: { ...s.sceneSettings.fog, ...(settings.fog || {}) },
  410. },
  411. };
  412. });
  413. return {
  414. ...saveHistory({ ...state, scenes: updatedScenes }),
  415. scenes: updatedScenes,
  416. };
  417. }),
  418. updateEnvironmentSettings: (settings) =>
  419. set((state) => {
  420. const updatedScenes = state.scenes.map((s) => {
  421. if (s.id !== state.activeSceneId) return s;
  422. return {
  423. ...s,
  424. environmentSettings: { ...s.environmentSettings, ...settings },
  425. };
  426. });
  427. return {
  428. ...saveHistory({ ...state, scenes: updatedScenes }),
  429. scenes: updatedScenes,
  430. };
  431. }),
  432. updateObjectGeneral: (id, updates) =>
  433. set((state) => {
  434. const updatedScenes = state.scenes.map((s) => {
  435. if (s.id !== state.activeSceneId) return s;
  436. return {
  437. ...s,
  438. objects: s.objects.map((o) => {
  439. if (o.id !== id) return o;
  440. if (o.type === "plane" && updates.scale) updates.scale[2] = 1;
  441. return { ...o, ...updates };
  442. }),
  443. };
  444. });
  445. return {
  446. ...saveHistory({ ...state, scenes: updatedScenes }),
  447. scenes: updatedScenes,
  448. };
  449. }),
  450. toggleObjectVisibility: (id) =>
  451. set((state) => {
  452. const updatedScenes = state.scenes.map((s) => {
  453. if (s.id !== state.activeSceneId) return s;
  454. return {
  455. ...s,
  456. objects: s.objects.map((o) =>
  457. o.id === id ? { ...o, visible: !o.visible } : o,
  458. ),
  459. };
  460. });
  461. return {
  462. ...saveHistory({ ...state, scenes: updatedScenes }),
  463. scenes: updatedScenes,
  464. };
  465. }),
  466. // UI state
  467. setTransformMode: (mode) => set({ transformMode: mode }),
  468. setIsPlaying: (playing) =>
  469. set((state) => ({
  470. isPlaying: playing,
  471. selectedObjectId: playing ? null : state.selectedObjectId,
  472. health: playing ? state.maxHealth : state.health,
  473. shield: playing ? state.maxShield : state.shield,
  474. isPromptOpen: playing ? false : state.isPromptOpen,
  475. })),
  476. setIsLoading: (loading) => set({ isLoading: loading }),
  477. setViewMode: (mode) => set({ viewMode: mode }),
  478. setIsPromptOpen: (open) => set({ isPromptOpen: open }),
  479. // History
  480. saveToHistory: () => set((state) => saveHistory(state)),
  481. undo: () =>
  482. set((state) => {
  483. if (state.history.past.length === 0) return state;
  484. const prev = state.history.past[state.history.past.length - 1]!;
  485. return {
  486. scenes: prev,
  487. history: {
  488. past: state.history.past.slice(0, -1),
  489. future: [state.scenes, ...state.history.future],
  490. },
  491. canUndo: state.history.past.length > 1,
  492. canRedo: true,
  493. };
  494. }),
  495. redo: () =>
  496. set((state) => {
  497. if (state.history.future.length === 0) return state;
  498. const next = state.history.future[0]!;
  499. return {
  500. scenes: next,
  501. history: {
  502. past: [...state.history.past, state.scenes],
  503. future: state.history.future.slice(1),
  504. },
  505. canUndo: true,
  506. canRedo: state.history.future.length > 1,
  507. };
  508. }),
  509. // Health
  510. setHealth: (health) =>
  511. set((state) => ({
  512. health: Math.max(0, Math.min(health, state.maxHealth)),
  513. })),
  514. setShield: (shield) =>
  515. set((state) => ({
  516. shield: Math.max(0, Math.min(shield, state.maxShield)),
  517. })),
  518. takeDamage: (amount) =>
  519. set((state) => {
  520. const now = Date.now();
  521. if (
  522. state.lastDamageTime &&
  523. now - state.lastDamageTime < state.damageCooldown * 1000
  524. )
  525. return state;
  526. let remainingShield = state.shield;
  527. let remainingHealth = state.health;
  528. let remaining = amount;
  529. if (remainingShield > 0) {
  530. if (remainingShield >= remaining) {
  531. remainingShield -= remaining;
  532. remaining = 0;
  533. } else {
  534. remaining -= remainingShield;
  535. remainingShield = 0;
  536. }
  537. }
  538. if (remaining > 0)
  539. remainingHealth = Math.max(0, remainingHealth - remaining);
  540. return {
  541. shield: remainingShield,
  542. health: remainingHealth,
  543. lastDamageTime: now,
  544. };
  545. }),
  546. }));