diff --git a/LittlestTokyo.glb b/LittlestTokyo.glb new file mode 100644 index 0000000..f2c7e04 Binary files /dev/null and b/LittlestTokyo.glb differ diff --git a/client/package-lock.json b/client/package-lock.json index 8c93526..92d3f95 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,7 +11,9 @@ "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.2", "@react-three/postprocessing": "^3.0.4", + "@react-three/rapier": "^2.2.0", "axios": "^1.13.2", + "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "^7.9.6", @@ -1186,6 +1188,27 @@ "three": ">=0.144.0" } }, + "node_modules/@react-three/rapier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-2.2.0.tgz", + "integrity": "sha512-mVsqbKXlGZoN+XrqdhzFZUQmy8pibEOVzl4k7LC+LHe84bQnYBSagy1Hvbda6bL1PJDdTFyiDiBk5buKFinNIQ==", + "dependencies": { + "@dimforge/rapier3d-compat": "0.19.2", + "suspend-react": "^0.1.3", + "three-stdlib": "^2.35.12" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.4", + "react": "^19", + "three": ">=0.159.0" + } + }, + "node_modules/@react-three/rapier/node_modules/@dimforge/rapier3d-compat": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.19.2.tgz", + "integrity": "sha512-AZHL1jqUF55QJkJyU1yKeh4ImX2J93bVLIezT1+o0FZqTix6O06MOaqpKoJ4MmbDCsoZmwO+qc471/SDMDm2AA==", + "license": "Apache-2.0" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -4039,7 +4062,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4198,7 +4220,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4349,7 +4370,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4675,7 +4695,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -4726,7 +4745,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-reconciler": { diff --git a/client/package.json b/client/package.json index 22b64e4..6db3440 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,9 @@ "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.2", "@react-three/postprocessing": "^3.0.4", + "@react-three/rapier": "^2.2.0", "axios": "^1.13.2", + "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "^7.9.6", diff --git a/client/public/models/LittlestTokyo.glb b/client/public/models/LittlestTokyo.glb new file mode 100644 index 0000000..f2c7e04 Binary files /dev/null and b/client/public/models/LittlestTokyo.glb differ diff --git a/client/src/CorridorScene.jsx b/client/src/CorridorScene.jsx index 60363c3..6025d08 100644 --- a/client/src/CorridorScene.jsx +++ b/client/src/CorridorScene.jsx @@ -50,7 +50,6 @@ export default function CorridorScene({ text }) { letterSpacing: 0.2, }} > - {/* הטקסט הדינמי המועבר מה-ThreeDemo (למשל: "starting Mission 1") */} {text} diff --git a/client/src/Missions/Mission1.jsx b/client/src/Missions/Mission1.jsx index e69de29..5f5037f 100644 --- a/client/src/Missions/Mission1.jsx +++ b/client/src/Missions/Mission1.jsx @@ -0,0 +1,290 @@ +/* eslint-disable react/no-unknown-property */ +import { useEffect, useMemo, useRef, useState } from "react"; +import * as THREE from "three"; +import { Html, Text } from "@react-three/drei"; + +import q1 from "../assets/q1.png"; +import q2 from "../assets/q2.png"; +import q3 from "../assets/q3.png"; + +export default function Mission1({ playerPose }) { + const ROOM_W = 70; + const ROOM_D = 70; + const WALL_VIS_H = 220; + const yCenter = -1.11 + WALL_VIS_H / 2; + + const GROUND_Y = -1.11; + const JUMP_TRIGGER_Y = GROUND_Y + 0.55; + + const wallMat = useMemo( + () => + new THREE.MeshStandardMaterial({ + color: "#2173bc", + transparent: true, + opacity: 0.28, + roughness: 0.85, + metalness: 0.0, + side: THREE.DoubleSide, + }), + [] + ); + + const leftWallX = -ROOM_W / 2 + 0.12; + + const [phase, setPhase] = useState("intro"); + const [answers, setAnswers] = useState([]); + + const wasAboveRef = useRef(false); + const phaseRef = useRef("intro"); + + useEffect(() => { + phaseRef.current = phase; + }, [phase]); + + const questions = useMemo( + () => [ + { + img: q1, + correctSide: "red", + correctMsg: "Correct. This is bullying.", + wrongMsg: "Not quite. This is bullying, not friendly.", + extra: "You can tell because the message is threatening and tries to control the other person.", + }, + { + img: q2, + correctSide: "red", + correctMsg: "Correct. This is bullying.", + wrongMsg: "Not quite. This is bullying, not friendly.", + extra: "Look for mocking, pressure, or making someone feel small.", + }, + { + img: q3, + correctSide: "green", + correctMsg: "Correct. This is friendly.", + wrongMsg: "Not quite. This is friendly, not bullying.", + extra: "Friendly messages feel safe and respectful, even when someone says no.", + }, + ], + [] + ); + + const qIndex = phase === "q1" || phase === "fb1" ? 0 : phase === "q2" || phase === "fb2" ? 1 : 2; + const currentQ = questions[qIndex]; + + const onJumpEvent = () => { + const side = playerPose?.z >= 0 ? "green" : "red"; + const p = phaseRef.current; + + if (p === "intro") { + setPhase("q1"); + return; + } + + if (p === "end") { + setAnswers([]); + setPhase("intro"); + return; + } + + if (p === "q1" || p === "q2" || p === "q3") { + const isCorrect = side === currentQ.correctSide; + setAnswers((prev) => [...prev, isCorrect]); + + if (p === "q1") setPhase("fb1"); + if (p === "q2") setPhase("fb2"); + if (p === "q3") setPhase("fb3"); + return; + } + + if (p === "fb1") setPhase("q2"); + if (p === "fb2") setPhase("q3"); + if (p === "fb3") setPhase("end"); + }; + + useEffect(() => { + const y = playerPose?.y ?? GROUND_Y; + const above = y > JUMP_TRIGGER_Y; + + if (above && !wasAboveRef.current) { + wasAboveRef.current = true; + onJumpEvent(); + } + + if (!above) { + wasAboveRef.current = false; + } + }, [playerPose]); + + const allCorrect = answers.length === 3 && answers.every(Boolean); + + const showImage = + phase === "q1" || + phase === "fb1" || + phase === "q2" || + phase === "fb2" || + phase === "q3" || + phase === "fb3"; + + const showFeedback = phase === "fb1" || phase === "fb2" || phase === "fb3"; + + const feedbackText = (() => { + if (!showFeedback) return ""; + const last = answers[answers.length - 1]; + return last ? currentQ.correctMsg : currentQ.wrongMsg; + })(); + + const feedbackExtra = showFeedback ? currentQ.extra : ""; + + return ( + + + + + {/* Walls */} + + + + + + + + + + + + + + + + + + + + + {/* Split floor */} + + + + + + + + + + + {/* Title */} + + + Friendly / Bullying + + + Jump on your answer to start + + + + + + + {showImage && ( + + + + {showFeedback ? "Feedback" : "Question"} - Jump on green or red + + + + + {showFeedback && ( + + {feedbackText} + {feedbackExtra} + Jump to continue + + )} + + + )} + + {phase === "end" && ( + + + + {allCorrect ? ( + <> + You won! + Great job spotting bullying patterns. + > + ) : ( + <> + Nice try + It’s ok - there is still more to learn. Jump to try again. + > + )} + Jump to restart + + + + )} + + ); +} + +function WallSign({ x, z, text }) { + return ( + + + + + + + {text} + + + ); +} diff --git a/client/src/Missions/Mission2.jsx b/client/src/Missions/Mission2.jsx index e69de29..054092a 100644 --- a/client/src/Missions/Mission2.jsx +++ b/client/src/Missions/Mission2.jsx @@ -0,0 +1,44 @@ +/* eslint-disable react/no-unknown-property */ +import { useMemo } from "react"; +import * as THREE from "three"; + +export default function Mission2Room() { + const ROOM_W = 70; + const ROOM_D = 70; + const WALL_VIS_H = 220; + const yCenter = -1.15 + WALL_VIS_H / 2; + + const wallMat = useMemo( + () => + new THREE.MeshStandardMaterial({ + color: "#fffffff1", + transparent: true, + opacity: 0.22, + roughness: 0.85, + metalness: 0.0, + side: THREE.DoubleSide, + }), + [] + ); + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/client/src/Missions/Mission3.jsx b/client/src/Missions/Mission3.jsx index e69de29..b93b7b4 100644 --- a/client/src/Missions/Mission3.jsx +++ b/client/src/Missions/Mission3.jsx @@ -0,0 +1,44 @@ +/* eslint-disable react/no-unknown-property */ +import { useMemo } from "react"; +import * as THREE from "three"; + +export default function Mission3Room() { + const ROOM_W = 70; + const ROOM_D = 70; + const WALL_VIS_H = 220; + const yCenter = -1.15 + WALL_VIS_H / 2; + + const wallMat = useMemo( + () => + new THREE.MeshStandardMaterial({ + color: "#f3f0ff", + transparent: true, + opacity: 0.22, + roughness: 0.85, + metalness: 0.0, + side: THREE.DoubleSide, + }), + [] + ); + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/client/src/Missions/Mission4.jsx b/client/src/Missions/Mission4.jsx index e69de29..e4a1b6d 100644 --- a/client/src/Missions/Mission4.jsx +++ b/client/src/Missions/Mission4.jsx @@ -0,0 +1,44 @@ +/* eslint-disable react/no-unknown-property */ +import { useMemo } from "react"; +import * as THREE from "three"; + +export default function Mission4Room() { + const ROOM_W = 70; + const ROOM_D = 70; + const WALL_VIS_H = 220; + const yCenter = -1.15 + WALL_VIS_H / 2; + + const wallMat = useMemo( + () => + new THREE.MeshStandardMaterial({ + color: "#e7ffe9", + transparent: true, + opacity: 0.22, + roughness: 0.85, + metalness: 0.0, + side: THREE.DoubleSide, + }), + [] + ); + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/client/src/Scene.jsx b/client/src/Scene.jsx index 97898cb..876c62f 100644 --- a/client/src/Scene.jsx +++ b/client/src/Scene.jsx @@ -5,34 +5,29 @@ import { OrbitControls } from "@react-three/drei"; import * as THREE from "three"; import Robot from "./components/Robot"; import { useKeyboard } from "./useKeyboard"; +import PropTypes from "prop-types"; +import Mission1Room from "./Missions/Mission1"; +import Mission2Room from "./Missions/Mission2"; +import Mission3Room from "./Missions/Mission3"; +import Mission4Room from "./Missions/Mission4"; -// --- פונקציות עזר מתמטיות --- -// הגבלת ערך בין מינימום למקסימום (למשל כדי שהרובוט לא יצא מהקירות) function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } -// מעבר רך בין ערכים (Linear Interpolation) -function lerp(a, b, t) { - return a + (b - a) * t; -} - -// --- רכיב האייקון של המשימה (MissionIcon) --- function MissionIcon({ position, tint = "#28f0e6" }) { const groupRef = useRef(); const linesRef = useRef(); const glowRef = useRef(); const auraRef = useRef(); - const tRef = useRef(Math.random() * 10); // זמן התחלתי רנדומלי כדי שהאייקונים לא יפעמו בסינכרון מושלם + const tRef = useRef(Math.random() * 10); - // יצירת טקסטורה של הילה (Aura) על הרצפה באמצעות Canvas const auraTex = useMemo(() => { const c = document.createElement("canvas"); c.width = 256; c.height = 256; const ctx = c.getContext("2d"); - // יצירת גרדיאנט מעגלי (Radial Gradient) const g = ctx.createRadialGradient(128, 128, 12, 128, 128, 120); g.addColorStop(0.0, "rgba(255,255,255,0.0)"); g.addColorStop(0.25, "rgba(255,255,255,0.22)"); @@ -47,24 +42,20 @@ function MissionIcon({ position, tint = "#28f0e6" }) { return tex; }, []); - // לוגיקת האנימציה של האייקון (מתבצע בכל פריים) useFrame((_, delta) => { tRef.current += delta; if (!groupRef.current) return; - // 1. אפקט ריחוף (Hover) מעלה ומטה בעזרת פונקציית Sin - const baseHover = 3; - const floatAmp = 0.18; - const floatSpeed = 1.0; + const baseHover = 3; + const floatAmp = 0.18; + const floatSpeed = 1.0; const floatY = Math.sin(tRef.current * floatSpeed) * floatAmp; groupRef.current.position.y = position[1] + baseHover + floatY; - // 2. סיבוב איטי סביב ציר ה-Y וטיה (Tilt) עדין למראה דינמי groupRef.current.rotation.y += delta * 0.6; groupRef.current.rotation.x = Math.sin(tRef.current * 0.6) * 0.06; groupRef.current.rotation.z = Math.cos(tRef.current * 0.6) * 0.04; - // 3. אפקט פעימה (Pulse) המשפיע על השקיפות והגודל const pulse = 0.5 + 0.5 * Math.sin(tRef.current * 2.2); if (linesRef.current?.material) { @@ -79,16 +70,14 @@ function MissionIcon({ position, tint = "#28f0e6" }) { if (auraRef.current?.material) { auraRef.current.material.opacity = 0.16 + pulse * 0.10; - auraRef.current.rotation.z += delta * 0.15; // סיבוב ההילה שעל הרצפה + auraRef.current.rotation.z += delta * 0.15; } }); return ( - {/* אור ניאון קטן שבוקע ממרכז האייקון */} - {/* קווי המתאר של הקוביה (Wireframe) */} - {/* נפח "Glow" פנימי שקוף */} - {/* מישור ההילה על הרצפה */} ); } +MissionIcon.propTypes = { + position: PropTypes.arrayOf(PropTypes.number).isRequired, + tint: PropTypes.string, +}; -// --- רכיב הסצנה המרכזי (Scene) --- export default function Scene({ roomId = "main", spawnKey, @@ -142,7 +132,6 @@ export default function Scene({ }) { const keys = useKeyboard(); - // הגדרות מימדי החדר ומרחק בטיחות מהקירות const ROOM_W = 70; const ROOM_D = 70; const WALL_PADDING = 2.5; @@ -155,37 +144,42 @@ export default function Scene({ const robotRef = useRef(); const controlsRef = useRef(); - // מיקום וסיבוב הרובוט (נשמר ב-Ref כדי למנוע רנדר מיותר של כל ה-React) const pos = useRef(new THREE.Vector3(0, -1.15, 0)); const yaw = useRef(0); - const SPEED = 7; + const velocityY = useRef(0); + const isGrounded = useRef(true); + const jumpLock = useRef(false); + + const SPEED = 10; + const JUMP_FORCE = 9.5; + const GRAVITY = 25; + const GROUND_Y = -1.15; - // הגדרות מצלמה (גובה, מרחק והיסט צידי למראה קולנועי) const CAM_HEIGHT = 4.1; const CAM_DISTANCE = 12.0; const CAM_SIDE = 10.2; - const CAM_LERP = 0.07; // מהירות ה"מרדף" של המצלמה אחרי הרובוט + const CAM_LERP = 0.07; const camTargetPos = useRef(new THREE.Vector3()); const camLookAt = useRef(new THREE.Vector3()); - // הגדרת מיקומי כל האייקונים בעולם - const ALL_ICONS = useMemo(() => [ - { id: "task1", label: "Mission 1", pos: new THREE.Vector3(16.5, -1.15, 14.0) }, - { id: "task2", label: "Mission 2", pos: new THREE.Vector3(-20.0, -1.15, 10.5) }, - { id: "task3", label: "Mission 3", pos: new THREE.Vector3(-14.5, -1.15, -21.0) }, - { id: "task4", label: "Mission 4", pos: new THREE.Vector3(22.0, -1.15, -10.0) }, - ], []); + const ALL_ICONS = useMemo( + () => [ + { id: "task1", label: "Mission 1", pos: new THREE.Vector3(16.5, -1.15, 14.0) }, + { id: "task2", label: "Mission 2", pos: new THREE.Vector3(-20.0, -1.15, 10.5) }, + { id: "task3", label: "Mission 3", pos: new THREE.Vector3(-14.5, -1.15, -21.0) }, + { id: "task4", label: "Mission 4", pos: new THREE.Vector3(22.0, -1.15, -10.0) }, + ], + [] + ); - // פילטור האייקונים להצגה לפי החדר הנוכחי const iconsToShow = useMemo(() => { if (roomId === "main") return ALL_ICONS; const found = ALL_ICONS.find((x) => x.id === roomId); return found ? [found] : []; }, [roomId, ALL_ICONS]); - // עדכון המפה החיצונית במיקומי האייקונים useEffect(() => { onIconsForMap?.( iconsToShow.map((ic) => ({ @@ -201,61 +195,101 @@ export default function Scene({ const poseTick = useRef(0); const lastNear = useRef("None"); - // אתחול מיקום הרובוט בעת כניסה לחדר (Spawn) useEffect(() => { pos.current.set(spawn?.x ?? 0, spawn?.y ?? -1.15, spawn?.z ?? 0); yaw.current = spawn?.yaw ?? 0; + + velocityY.current = 0; + isGrounded.current = true; + jumpLock.current = false; + triggerCooldown.current = 1.6; lastNear.current = "None"; onNearLabel?.("None"); }, [spawnKey, spawn, onNearLabel]); - // לולאת העדכון הראשית של ה-3D + const wallMat = useMemo(() => { + return new THREE.MeshBasicMaterial({ + color: "#ffffff", + transparent: true, + opacity: 0.12, + depthWrite: false, + side: THREE.DoubleSide, + }); + }, []); + useFrame(({ camera }, delta) => { const k = keys.current; + if (triggerCooldown.current > 0) triggerCooldown.current -= delta; + // Jump trigger on Space press (with lock so holding Space doesn't re-trigger) + if (k.Space) { + if (!jumpLock.current && isGrounded.current && inputEnabled) { + jumpLock.current = true; + isGrounded.current = false; + velocityY.current = JUMP_FORCE; + + if (robotRef.current?.userData?.setAction) { + robotRef.current.userData.setAction("Jump"); + } + } + } else { + jumpLock.current = false; + } + + // Gravity + velocityY.current -= GRAVITY * delta; + pos.current.y += velocityY.current * delta; + + // Ground collision + if (pos.current.y <= GROUND_Y) { + pos.current.y = GROUND_Y; + velocityY.current = 0; + isGrounded.current = true; + } + + // Movement + animation if (!inputEnabled) { - // אם התנועה חסומה (למשל בזמן מעבר חדר), הרובוט עובר למצב המתנה if (robotRef.current?.userData?.setAction) robotRef.current.userData.setAction("Idle"); } else { - // 1. חישוב סיבוב (Rotation) let turn = 0; if (k.ArrowLeft || k.KeyA) turn += 1; if (k.ArrowRight || k.KeyD) turn -= 1; yaw.current += turn * 2.2 * delta; - // 2. חישוב תנועה (Movement) let forward = 0; if (k.ArrowUp || k.KeyW) forward = 1; if (k.ArrowDown || k.KeyS) forward = -1; const moving = forward !== 0; + if (moving) { - // המרת זווית הסיבוב לוקטור תנועה במרחב (Trigonometry) const vx = Math.sin(yaw.current) * forward; const vz = Math.cos(yaw.current) * forward; const nextX = pos.current.x + vx * SPEED * delta; const nextZ = pos.current.z + vz * SPEED * delta; - // מניעת יציאה מגבולות החדר pos.current.x = clamp(nextX, minX, maxX); pos.current.z = clamp(nextZ, minZ, maxZ); + // Run instead of Walk if (robotRef.current?.userData?.setAction) robotRef.current.userData.setAction("Walk"); } else { - if (robotRef.current?.userData?.setAction) robotRef.current.userData.setAction("Idle"); + // If in air, keep Jump (do not force Idle) + if (isGrounded.current) { + if (robotRef.current?.userData?.setAction) robotRef.current.userData.setAction("Idle"); + } } } - // עדכון המודל התלת-ממדי של הרובוט if (robotRef.current) { robotRef.current.position.set(pos.current.x, pos.current.y, pos.current.z); robotRef.current.rotation.y = yaw.current; } - // 3. בדיקת קרבה למשימות (Collision/Trigger Logic) + // Mission triggers if (inputEnabled) { let nearestDist = Infinity; let nearestLabel = "None"; @@ -263,16 +297,16 @@ export default function Scene({ for (const ic of iconsToShow) { const d = pos.current.distanceTo(ic.pos); - if (d < nearestDist) { nearestDist = d; nearestLabel = ic.label; } - - // הפעלת המשימה אם הרובוט מספיק קרוב + if (d < nearestDist) { + nearestDist = d; + nearestLabel = ic.label; + } if (d < triggerDist && triggerCooldown.current <= 0) { triggerCooldown.current = 1.4; onMissionTrigger?.(ic.id); } } - // עדכון ה-UI על המשימה הקרובה ביותר const nearLabel = nearestDist < 11 ? nearestLabel : "None"; if (nearLabel !== lastNear.current) { lastNear.current = nearLabel; @@ -280,99 +314,90 @@ export default function Scene({ } } - // דיווח מיקום למפה החיצונית (בתדירות נמוכה יותר לשיפור ביצועים) poseTick.current += delta; if (poseTick.current > 0.06) { poseTick.current = 0; - onPose?.({ x: pos.current.x, z: pos.current.z, yaw: yaw.current, roomW: ROOM_W, roomD: ROOM_D, roomId }); + onPose?.({ + x: pos.current.x, + z: pos.current.z, + yaw: yaw.current, + roomW: ROOM_W, + roomD: ROOM_D, + roomId, + }); } - // 4. לוגיקת מצלמה עוקבת (Chase Camera) - camLookAt.current.set(pos.current.x, 1.6, pos.current.z); // המצלמה תמיד מסתכלת על הרובוט + camLookAt.current.set(pos.current.x, 1.6, pos.current.z); - // חישוב המיקום האידיאלי של המצלמה מאחורי ובצד הרובוט const backX = Math.sin(yaw.current) * CAM_DISTANCE; const backZ = Math.cos(yaw.current) * CAM_DISTANCE; const rightX = Math.sin(yaw.current + Math.PI / 2) * CAM_SIDE; const rightZ = Math.cos(yaw.current + Math.PI / 2) * CAM_SIDE; - camTargetPos.current.set( - pos.current.x - backX + rightX, - CAM_HEIGHT, - pos.current.z - backZ + rightZ - ); + camTargetPos.current.set(pos.current.x - backX + rightX, CAM_HEIGHT, pos.current.z - backZ + rightZ); - // הגבלת המצלמה שלא תצא מהקירות const CAM_PADDING = 2.5; camTargetPos.current.x = clamp(camTargetPos.current.x, -ROOM_W / 2 + CAM_PADDING, ROOM_W / 2 - CAM_PADDING); camTargetPos.current.z = clamp(camTargetPos.current.z, -ROOM_D / 2 + CAM_PADDING, ROOM_D / 2 - CAM_PADDING); - // תנועת מצלמה חלקה (Lerp) ומיקוד camera.position.lerp(camTargetPos.current, CAM_LERP); camera.lookAt(camLookAt.current); }); - // יצירת חומר שקוף לקירות (משותף לכל הקירות) - const wallMat = useMemo(() => { - return new THREE.MeshBasicMaterial({ - color: "#ffffff", - transparent: true, - opacity: 0.12, - depthWrite: false, - side: THREE.DoubleSide, - }); - }, []); - return ( <> - {/* תאורה סביבתית ותאורת כיוון (שמש) ליצירת צללים */} - {/* בניית ארבעת קירות החדר */} - {(() => { - const WALL_VIS_H = 220; // גובה ויזואלי של הקירות - const yCenter = -1.15 + WALL_VIS_H / 2; - const W = ROOM_W; - const D = ROOM_D; - return ( - <> - - - - - - - - - - - - - - - - - > - ); - })()} - - {/* הרצפה - מקבלת צללים מהרובוט */} + {roomId === "task1" ? ( + + ) : roomId === "task2" ? ( + + ) : roomId === "task3" ? ( + + ) : roomId === "task4" ? ( + + ) : ( + (() => { + const WALL_VIS_H = 220; + const yCenter = -1.15 + WALL_VIS_H / 2; + const W = ROOM_W; + const D = ROOM_D; + return ( + <> + + + + + + + + + + + + + + + + + > + ); + })() + )} + - {/* רינדור האייקונים */} {iconsToShow.map((ic) => ( ))} - {/* מודל הרובוט */} - {/* בקרת מצלמה (מושבתת לשליטה ידנית, משמשת רק למיקוד) */} > ); -} \ No newline at end of file +} diff --git a/client/src/ThreeDemo.jsx b/client/src/ThreeDemo.jsx index cf7a49c..5ac6e61 100644 --- a/client/src/ThreeDemo.jsx +++ b/client/src/ThreeDemo.jsx @@ -5,6 +5,8 @@ import * as THREE from "three"; import Scene from "./Scene"; import CorridorScene from "./CorridorScene"; + + export default function ThreeDemo() { // --- ניהול מצבי החדרים --- const [room, setRoom] = useState("main"); // החדר הנוכחי שבו נמצא המשתמש diff --git a/client/src/assets/q1.png b/client/src/assets/q1.png new file mode 100644 index 0000000..acf1dff Binary files /dev/null and b/client/src/assets/q1.png differ diff --git a/client/src/assets/q2.png b/client/src/assets/q2.png new file mode 100644 index 0000000..27869ea Binary files /dev/null and b/client/src/assets/q2.png differ diff --git a/client/src/assets/q3.png b/client/src/assets/q3.png new file mode 100644 index 0000000..affdbdf Binary files /dev/null and b/client/src/assets/q3.png differ diff --git a/client/src/useKeyboard.js b/client/src/useKeyboard.js index 8507ef6..800cf0d 100644 --- a/client/src/useKeyboard.js +++ b/client/src/useKeyboard.js @@ -10,6 +10,7 @@ export function useKeyboard() { KeyS: false, KeyA: false, KeyD: false, + Space: false, }); useEffect(() => {