character-interaction.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- @types/three version mismatch
  29. camera.getWorldPosition(cameraPos as any);
  30. const characters = objects.filter(
  31. (obj) => obj.type === "character" && obj.visible,
  32. );
  33. let closest: { id: string; role: string } | null = null;
  34. let closestDist = Infinity;
  35. for (const character of characters) {
  36. const charPos = new THREE.Vector3(...character.position);
  37. const dist = cameraPos.distanceTo(charPos);
  38. if (dist < 3 && dist < closestDist) {
  39. closestDist = dist;
  40. closest = {
  41. id: character.id,
  42. role: character.character?.role || "villager",
  43. };
  44. }
  45. }
  46. if (closest) {
  47. if (!nearbyCharacter || nearbyCharacter.id !== closest.id) {
  48. setNearbyCharacter(closest);
  49. window.dispatchEvent(
  50. new CustomEvent("character-nearby", {
  51. detail: { id: closest.id, role: closest.role },
  52. }),
  53. );
  54. }
  55. } else if (nearbyCharacter) {
  56. setNearbyCharacter(null);
  57. window.dispatchEvent(new CustomEvent("character-far"));
  58. }
  59. };
  60. const intervalId = setInterval(checkProximity, 200);
  61. const triggerInteract = () => {
  62. if (nearbyCharacter) {
  63. window.dispatchEvent(
  64. new CustomEvent("character-interact", {
  65. detail: {
  66. id: nearbyCharacter.id,
  67. role: nearbyCharacter.role,
  68. },
  69. }),
  70. );
  71. }
  72. };
  73. const handleKeyDown = (e: KeyboardEvent) => {
  74. if (e.key === "e" || e.key === "E") {
  75. triggerInteract();
  76. }
  77. };
  78. const handleRequestInteract = () => {
  79. triggerInteract();
  80. };
  81. window.addEventListener("keydown", handleKeyDown);
  82. window.addEventListener(
  83. "request-character-interact",
  84. handleRequestInteract,
  85. );
  86. return () => {
  87. clearInterval(intervalId);
  88. window.removeEventListener("keydown", handleKeyDown);
  89. window.removeEventListener(
  90. "request-character-interact",
  91. handleRequestInteract,
  92. );
  93. };
  94. }, [camera, isPlaying, nearbyCharacter, objects]);
  95. return null;
  96. }