character-interaction.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. "use client";
  2. import { useEffect, useMemo, useState } from "react";
  3. import { useThree } from "@react-three/fiber";
  4. import * as THREE from "three";
  5. import { useEditorStore } from "@/lib/store";
  6. export function CharacterInteraction() {
  7. const [nearbyCharacter, setNearbyCharacter] = useState<{
  8. id: string;
  9. role: string;
  10. } | null>(null);
  11. const { camera } = useThree();
  12. const scenes = useEditorStore((s) => s.scenes);
  13. const activeSceneId = useEditorStore((s) => s.activeSceneId);
  14. const isPlaying = useEditorStore((s) => s.isPlaying);
  15. const activeScene = scenes.find((s) => s.id === activeSceneId) || scenes[0];
  16. const objects = useMemo(
  17. () => (activeScene ? activeScene.objects : []),
  18. [activeScene],
  19. );
  20. useEffect(() => {
  21. if (!isPlaying) {
  22. setNearbyCharacter(null);
  23. return;
  24. }
  25. const checkProximity = () => {
  26. if (!camera) return;
  27. const cameraPos = new THREE.Vector3();
  28. camera.getWorldPosition(cameraPos);
  29. const characters = objects.filter(
  30. (obj) => obj.type === "character" && obj.visible,
  31. );
  32. let closest: { id: string; role: string } | null = null;
  33. let closestDist = Infinity;
  34. for (const character of characters) {
  35. const charPos = new THREE.Vector3(...character.position);
  36. const dist = cameraPos.distanceTo(charPos);
  37. if (dist < 3 && dist < closestDist) {
  38. closestDist = dist;
  39. closest = {
  40. id: character.id,
  41. role: character.character?.role || "villager",
  42. };
  43. }
  44. }
  45. if (closest) {
  46. if (!nearbyCharacter || nearbyCharacter.id !== closest.id) {
  47. setNearbyCharacter(closest);
  48. window.dispatchEvent(
  49. new CustomEvent("character-nearby", {
  50. detail: { id: closest.id, role: closest.role },
  51. }),
  52. );
  53. }
  54. } else if (nearbyCharacter) {
  55. setNearbyCharacter(null);
  56. window.dispatchEvent(new CustomEvent("character-far"));
  57. }
  58. };
  59. const intervalId = setInterval(checkProximity, 200);
  60. const triggerInteract = () => {
  61. if (nearbyCharacter) {
  62. window.dispatchEvent(
  63. new CustomEvent("character-interact", {
  64. detail: {
  65. id: nearbyCharacter.id,
  66. role: nearbyCharacter.role,
  67. },
  68. }),
  69. );
  70. }
  71. };
  72. const handleKeyDown = (e: KeyboardEvent) => {
  73. if (e.key === "e" || e.key === "E") {
  74. triggerInteract();
  75. }
  76. };
  77. const handleRequestInteract = () => {
  78. triggerInteract();
  79. };
  80. window.addEventListener("keydown", handleKeyDown);
  81. window.addEventListener(
  82. "request-character-interact",
  83. handleRequestInteract,
  84. );
  85. return () => {
  86. clearInterval(intervalId);
  87. window.removeEventListener("keydown", handleKeyDown);
  88. window.removeEventListener(
  89. "request-character-interact",
  90. handleRequestInteract,
  91. );
  92. };
  93. }, [camera, isPlaying, nearbyCharacter, objects]);
  94. return null;
  95. }