253 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const lockpickContainer = document.querySelector("#lockpick-container"),
 | |
|     pin = document.querySelector("#pin"),
 | |
|     cyl = document.querySelector("#cylinder"),
 | |
|     driver = document.querySelector("#driver");
 | |
| 
 | |
| const minRot = -90,
 | |
|     maxRot = 90,
 | |
|     solvePadding = 4,
 | |
|     maxDistFromSolve = 45,
 | |
|     mouseSmoothing = 2,
 | |
|     keyRepeatRate = 25,
 | |
|     cylRotSpeed = 3,
 | |
|     pinDamage = 20,
 | |
|     pinDamageInterval = 150;
 | |
| 
 | |
| let solveDeg = Math.random() * (maxRot - minRot + 1) + minRot,
 | |
|     pinRot = 0,
 | |
|     cylRot = 0,
 | |
|     lastMousePos = 0,
 | |
|     pinHealth = 100,
 | |
|     numPins = 1,
 | |
|     userPushingCyl = false,
 | |
|     gameOver = false,
 | |
|     gamePaused = false,
 | |
|     cylRotationInterval,
 | |
|     pinLastDamaged;
 | |
| 
 | |
| const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
 | |
| const convertRanges = (value, oldMin, oldMax, newMin, newMax) => ((value - oldMin) * (newMax - newMin)) / (oldMax - oldMin) + newMin;
 | |
| 
 | |
| const closeLockpick = () => {
 | |
|     solveDeg = Math.random() * (maxRot - minRot + 1) + minRot;
 | |
|     pinRot = 0;
 | |
|     cylRot = 0;
 | |
|     lastMousePos = 0;
 | |
|     pinHealth = 100;
 | |
|     numPins = 1;
 | |
|     userPushingCyl = false;
 | |
|     gameOver = true;
 | |
|     gamePaused = true;
 | |
|     lockpickContainer.style.display = "none";
 | |
|     if (cylRotationInterval) {
 | |
|         cancelAnimationFrame(cylRotationInterval);
 | |
|         cylRotationInterval = null;
 | |
|     }
 | |
| };
 | |
| 
 | |
| const unlock = () => {
 | |
|     closeLockpick();
 | |
|     fetch(`https://${GetParentResourceName()}/lockpickFinish`, {
 | |
|         method: "POST",
 | |
|         headers: { "Content-Type": "application/json" },
 | |
|         body: JSON.stringify({ success: true }),
 | |
|     });
 | |
| };
 | |
| 
 | |
| const pushCyl = () => {
 | |
|     cancelAnimationFrame(cylRotationInterval);
 | |
|     userPushingCyl = true;
 | |
| 
 | |
|     let distFromSolve = Math.abs(pinRot - solveDeg) - solvePadding;
 | |
|     distFromSolve = clamp(distFromSolve, 0, maxDistFromSolve);
 | |
| 
 | |
|     let cylRotationAllowance = convertRanges(distFromSolve, 0, maxDistFromSolve, maxRot, maxRot * 0.02);
 | |
| 
 | |
|     function updateCylinderRotation() {
 | |
|         if (!userPushingCyl || cylRot >= maxRot || gameOver) {
 | |
|             cancelAnimationFrame(cylRotationInterval);
 | |
|             if (cylRot >= maxRot) {
 | |
|                 unlock();
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         cylRot += cylRotSpeed;
 | |
|         cylRot = Math.min(cylRot, cylRotationAllowance);
 | |
| 
 | |
|         if (cylRot >= cylRotationAllowance) {
 | |
|             damagePin();
 | |
|         }
 | |
| 
 | |
|         cyl.style.transform = `translate(-50%, -50%) rotateZ(${cylRot}deg)`;
 | |
|         driver.style.transform = `rotateZ(${cylRot}deg)`;
 | |
| 
 | |
|         cylRotationInterval = requestAnimationFrame(updateCylinderRotation);
 | |
|     }
 | |
| 
 | |
|     cylRotationInterval = requestAnimationFrame(updateCylinderRotation);
 | |
| };
 | |
| 
 | |
| const unpushCyl = () => {
 | |
|     userPushingCyl = false;
 | |
|     cancelAnimationFrame(cylRotationInterval);
 | |
| 
 | |
|     function updateCylinderRotationBackward() {
 | |
|         if (cylRot <= 0 || gameOver) {
 | |
|             cylRot = 0;
 | |
|             cancelAnimationFrame(cylRotationInterval);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         cylRot -= cylRotSpeed;
 | |
|         cylRot = Math.max(cylRot, 0);
 | |
| 
 | |
|         cyl.style.transform = `translate(-50%, -50%) rotateZ(${cylRot}deg)`;
 | |
|         driver.style.transform = `rotateZ(${cylRot}deg)`;
 | |
| 
 | |
|         cylRotationInterval = requestAnimationFrame(updateCylinderRotationBackward);
 | |
|     }
 | |
| 
 | |
|     cylRotationInterval = requestAnimationFrame(updateCylinderRotationBackward);
 | |
| };
 | |
| 
 | |
| const damagePin = () => {
 | |
|     if (!pinLastDamaged || Date.now() - pinLastDamaged > pinDamageInterval) {
 | |
|         pinHealth -= pinDamage;
 | |
|         pinLastDamaged = Date.now();
 | |
| 
 | |
|         const keyframes = [{ transform: `rotateZ(${pinRot}deg)` }, { transform: `rotateZ(${pinRot - 2}deg)` }, { transform: `rotateZ(${pinRot}deg)` }];
 | |
| 
 | |
|         const options = {
 | |
|             duration: pinDamageInterval / 2,
 | |
|             easing: "ease-out",
 | |
|         };
 | |
| 
 | |
|         pin.animate(keyframes, options);
 | |
| 
 | |
|         if (pinHealth <= 0) {
 | |
|             breakPin();
 | |
|         }
 | |
|     }
 | |
| };
 | |
| 
 | |
| const reset = () => {
 | |
|     cylRot = 0;
 | |
|     pinHealth = 100;
 | |
|     pinRot = 0;
 | |
| 
 | |
|     pin.style.transform = `rotateZ(${pinRot}deg)`;
 | |
|     cyl.style.transform = `translate(-50%, -50%) rotateZ(${cylRot}deg)`;
 | |
|     driver.style.transform = `rotateZ(${cylRot}deg)`;
 | |
| 
 | |
|     const pinTop = pin.querySelector(".top");
 | |
|     const pinBott = pin.querySelector(".bott");
 | |
| 
 | |
|     [pinTop, pinBott].forEach((el) => {
 | |
|         if (el) {
 | |
|             el.style.transform = "rotateZ(0deg) translateX(0) translateY(0)";
 | |
|             el.style.opacity = "1";
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     if (cylRotationInterval) {
 | |
|         cancelAnimationFrame(cylRotationInterval);
 | |
|         cylRotationInterval = null;
 | |
|     }
 | |
| };
 | |
| 
 | |
| const outOfPins = () => {
 | |
|     closeLockpick();
 | |
|     fetch(`https://${GetParentResourceName()}/lockpickFinish`, {
 | |
|         method: "POST",
 | |
|         headers: { "Content-Type": "application/json" },
 | |
|         body: JSON.stringify({ success: false }),
 | |
|     });
 | |
| };
 | |
| 
 | |
| const breakPin = () => {
 | |
|     gamePaused = true;
 | |
|     cancelAnimationFrame(cylRotationInterval);
 | |
| 
 | |
|     numPins--;
 | |
| 
 | |
|     const pinTop = pin.querySelector(".top");
 | |
|     const pinBott = pin.querySelector(".bott");
 | |
| 
 | |
|     const animateOptions = {
 | |
|         duration: 700,
 | |
|     };
 | |
| 
 | |
|     pinTop.animate(
 | |
|         [
 | |
|             { transform: "rotateZ(0deg) translateX(0) translateY(0)", opacity: 1 },
 | |
|             { transform: "rotateZ(-400deg) translateX(-200px) translateY(-100px)", opacity: 0 },
 | |
|         ],
 | |
|         animateOptions
 | |
|     );
 | |
| 
 | |
|     const bottomAnimation = pinBott.animate(
 | |
|         [
 | |
|             { transform: "rotateZ(0deg) translateX(0) translateY(0)", opacity: 1 },
 | |
|             { transform: "rotateZ(400deg) translateX(200px) translateY(100px)", opacity: 0 },
 | |
|         ],
 | |
|         animateOptions
 | |
|     );
 | |
| 
 | |
|     bottomAnimation.onfinish = () => {
 | |
|         if (numPins > 0) {
 | |
|             gamePaused = false;
 | |
|             reset();
 | |
|         } else {
 | |
|             outOfPins();
 | |
|         }
 | |
|     };
 | |
| };
 | |
| 
 | |
| document.addEventListener("mousemove", (e) => {
 | |
|     if (!gameOver && !gamePaused) {
 | |
|         let pinRotChange = (e.clientX - lastMousePos) / mouseSmoothing;
 | |
|         pinRot += pinRotChange;
 | |
|         pinRot = clamp(pinRot, minRot, maxRot);
 | |
|         pin.style.transform = `rotateZ(${pinRot}deg)`;
 | |
|     }
 | |
|     lastMousePos = e.clientX;
 | |
| });
 | |
| 
 | |
| document.addEventListener("mouseleave", () => (lastMousePos = 0));
 | |
| 
 | |
| const keyActionMap = {
 | |
|     w: pushCyl,
 | |
|     a: pushCyl,
 | |
|     s: pushCyl,
 | |
|     d: pushCyl,
 | |
|     escape: () => {
 | |
|         closeLockpick();
 | |
|         fetch(`https://${GetParentResourceName()}/lockpickExit`, { method: "POST" }).catch(console.error);
 | |
|     },
 | |
| };
 | |
| 
 | |
| document.addEventListener("keydown", (e) => {
 | |
|     let action = keyActionMap[e.key.toLowerCase()];
 | |
|     if (action && !userPushingCyl && !gameOver && !gamePaused) {
 | |
|         action();
 | |
|     }
 | |
| });
 | |
| 
 | |
| document.addEventListener("keyup", (e) => {
 | |
|     let action = keyActionMap[e.key.toLowerCase()];
 | |
|     if (action && !gameOver) {
 | |
|         unpushCyl();
 | |
|     }
 | |
| });
 | |
| 
 | |
| window.addEventListener("message", (event) => {
 | |
|     const eventData = event.data;
 | |
|     if (eventData.action === "startLockpick") {
 | |
|         lockpickContainer.style.display = "block";
 | |
|         numPins = eventData.pins;
 | |
|         gameOver = false;
 | |
|         gamePaused = false;
 | |
|         reset();
 | |
|     }
 | |
| });
 | 
