diff --git a/app/components/NDS.vue b/app/components/NDS.vue index 418e82e..c4ed841 100644 --- a/app/components/NDS.vue +++ b/app/components/NDS.vue @@ -22,8 +22,23 @@ const scene = computed(() => model.value?.scene); let topScreenTexture: THREE.CanvasTexture | null = null; let bottomScreenTexture: THREE.CanvasTexture | null = null; -let topScreenMesh: THREE.Mesh | null = null; -let bottomScreenMesh: THREE.Mesh | null = null; + +/// meshes /// +// screens +const TOP_SCREEN = "Object_9"; +const BOTTOM_SCREEN = "Object_28"; + +// buttons +const CROSS_BUTTON = "Object_21"; + +const meshes = new Map(); + +const requireMesh = (key: string): THREE.Mesh => { + const mesh = meshes.get(key); + if (!mesh) throw new Error(`Missing mesh '${key}'`); + + return mesh; +}; const { camera, renderer } = useTresContext(); @@ -36,11 +51,15 @@ watch( topScreenTexture.minFilter = THREE.NearestFilter; topScreenTexture.magFilter = THREE.NearestFilter; topScreenTexture.flipY = false; + topScreenTexture.repeat.set(1, 1024 / 404); + topScreenTexture.offset.set(0, -4 / 1024); bottomScreenTexture = new THREE.CanvasTexture(props.bottomScreenCanvas); bottomScreenTexture.minFilter = THREE.NearestFilter; bottomScreenTexture.magFilter = THREE.NearestFilter; bottomScreenTexture.flipY = false; + bottomScreenTexture.repeat.set(1, 1024 / 532); + bottomScreenTexture.offset.set(0, -1024 / 532 + 1); }, { immediate: true }, ); @@ -48,44 +67,86 @@ watch( watch(scene, () => { if (!scene.value) return; + meshes.clear(); + + scene.value.scale.set(100, 100, 100); + scene.value.traverse((child) => { if (child instanceof THREE.Mesh) { - const material = child.material as THREE.Material; - - if (material.name?.includes("screen_up") && topScreenTexture) { - topScreenTexture.repeat.set(1, 1024 / 404); - topScreenTexture.offset.set(0, -4 / 1024); - child.material = new THREE.MeshStandardMaterial({ - map: topScreenTexture, - emissive: new THREE.Color(0x222222), - emissiveIntensity: 0.5, - }); - topScreenMesh = child; - } else if ( - material.name?.includes("screen_down") && - bottomScreenTexture - ) { - bottomScreenTexture.repeat.set(1, 1024 / 532); - bottomScreenTexture.offset.set(0, -1024 / 532 + 1); - child.material = new THREE.MeshStandardMaterial({ - map: bottomScreenTexture, - emissive: new THREE.Color(0x222222), - emissiveIntensity: 0.5, - }); - bottomScreenMesh = child; - } + meshes.set(child.name, child); } }); + + if (!topScreenTexture || !bottomScreenTexture) + throw new Error( + "topScreenTexture and bottomScreenTexture should be initialized", + ); + + requireMesh(TOP_SCREEN).material = new THREE.MeshStandardMaterial({ + map: topScreenTexture, + emissive: new THREE.Color(0x222222), + emissiveIntensity: 0.5, + }); + + requireMesh(BOTTOM_SCREEN).material = new THREE.MeshStandardMaterial({ + map: bottomScreenTexture, + emissive: new THREE.Color(0x222222), + emissiveIntensity: 0.5, + }); }); const { onRender } = useLoop(); -onRender(() => { +const physicalButtonsDown = new Set(); +let mousePressedButton: string | null = null; + +const keyButtonMappings: [key: string, physicalButton: string][] = [ + ["ArrowUp", "UP"], + ["ArrowDown", "DOWN"], + ["ArrowLeft", "LEFT"], + ["ArrowRight", "RIGHT"], +]; + +const keyToButton = new Map(keyButtonMappings); +const buttonToKey = new Map(keyButtonMappings.map(([k, b]) => [b, k])); + +useKeyDown((key) => { + const button = keyToButton.get(key); + if (button) physicalButtonsDown.add(button); +}); + +useKeyUp((key) => { + const button = keyToButton.get(key); + if (button) physicalButtonsDown.delete(button); +}); + +onRender(({ delta }) => { if (topScreenTexture) topScreenTexture.needsUpdate = true; if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true; + + const crossButton = meshes.get(CROSS_BUTTON); + if (crossButton) { + const PRESS_ANGLE = Math.PI / 28; + const PRESS_SPEED = 150; + + const targetRotation = new THREE.Vector3(0); + if (physicalButtonsDown.has("UP")) targetRotation.setX(-PRESS_ANGLE); + if (physicalButtonsDown.has("RIGHT")) targetRotation.setZ(-PRESS_ANGLE); + if (physicalButtonsDown.has("DOWN")) targetRotation.setX(PRESS_ANGLE); + if (physicalButtonsDown.has("LEFT")) targetRotation.setZ(PRESS_ANGLE); + + const lerpFactor = Math.min(delta * PRESS_SPEED, 1); + const currentRotation = new THREE.Vector3().setFromEuler( + crossButton.rotation, + ); + currentRotation.lerp(targetRotation, lerpFactor); + crossButton.rotation.setFromVector3(currentRotation); + } }); const handleClick = (event: MouseEvent) => { + if (!scene.value) return; + const domElement = renderer.instance.domElement; const rect = domElement.getBoundingClientRect(); @@ -98,41 +159,87 @@ const handleClick = (event: MouseEvent) => { camera.activeCamera.value, ); - if (topScreenMesh) { - const intersects = raycaster.intersectObject(topScreenMesh); - if (intersects[0]) { - const uv = intersects[0].uv; - if (uv) { - const x = Math.floor(uv.x * 256); - const y = Math.floor(uv.y * (1024 / 404) * 192); - emit("topScreenClick", x, y); - return; - } - } - } + const intersects = raycaster.intersectObjects(scene.value.children, true); + const intersection = intersects[0]; + if (!intersection?.uv) return; - if (bottomScreenMesh) { - const intersects = raycaster.intersectObject(bottomScreenMesh); - if (intersects[0]) { - const uv = intersects[0].uv; - if (uv) { - const x = Math.floor(uv.x * 256); - const y = Math.floor(192 - (1 - uv.y) * (1024 / 532) * 192); + switch (intersection.object.name) { + case TOP_SCREEN: + case BOTTOM_SCREEN: { + const x = Math.floor(intersection.uv.x * 256); + + if (intersection.object.name === TOP_SCREEN) { + const y = Math.floor(intersection.uv.y * (1024 / 404) * 192); + emit("topScreenClick", x, y); + } else if (intersection.object.name === BOTTOM_SCREEN) { + const y = Math.floor( + 192 - (1 - intersection.uv.y) * (1024 / 532) * 192, + ); emit("bottomScreenClick", x, y); } + break; } + + case CROSS_BUTTON: { + const localPos = intersection.point + .clone() + .sub(intersection.object.getWorldPosition(new THREE.Vector3())); + + const MIN_DIST = 0.45; + let button: string | null = null; + + if (localPos.z <= -MIN_DIST) button = "UP"; + else if (localPos.x >= MIN_DIST) button = "RIGHT"; + else if (localPos.z >= MIN_DIST) button = "DOWN"; + else if (localPos.x <= -MIN_DIST) button = "LEFT"; + + if (button) { + if (mousePressedButton) { + physicalButtonsDown.delete(mousePressedButton); + const prevKey = buttonToKey.get(mousePressedButton); + if (prevKey) { + window.dispatchEvent(new KeyboardEvent("keyup", { key: prevKey })); + } + } + + physicalButtonsDown.add(button); + mousePressedButton = button; + + const key = buttonToKey.get(button); + if (key) { + window.dispatchEvent(new KeyboardEvent("keydown", { key })); + } + } + + break; + } + } +}; + +const handleMouseUp = () => { + if (mousePressedButton) { + physicalButtonsDown.delete(mousePressedButton); + + const key = buttonToKey.get(mousePressedButton); + if (key) { + window.dispatchEvent(new KeyboardEvent("keyup", { key })); + } + + mousePressedButton = null; } }; onMounted(() => { if (renderer) { - renderer.instance.domElement.addEventListener("click", handleClick); + renderer.instance.domElement.addEventListener("mousedown", handleClick); + renderer.instance.domElement.addEventListener("mouseup", handleMouseUp); } }); onUnmounted(() => { if (renderer) { - renderer.instance.domElement.removeEventListener("click", handleClick); + renderer.instance.domElement.removeEventListener("mousedown", handleClick); + renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp); } topScreenTexture?.dispose(); bottomScreenTexture?.dispose(); diff --git a/app/composables/useKeyUp.ts b/app/composables/useKeyUp.ts new file mode 100644 index 0000000..ed78e9a --- /dev/null +++ b/app/composables/useKeyUp.ts @@ -0,0 +1,15 @@ +export type KeyUpCallback = (key: string) => void; + +export const useKeyUp = (callback: KeyUpCallback) => { + const handleKeyUp = (event: KeyboardEvent) => { + callback(event.key); + }; + + onMounted(() => { + window.addEventListener("keyup", handleKeyUp); + }); + + onUnmounted(() => { + window.removeEventListener("keyup", handleKeyUp); + }); +};