koin.js 1.0.15 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +22 -6
- package/dist/index.d.ts +22 -6
- package/dist/index.js +1059 -545
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1060 -546
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +64 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -334,9 +334,11 @@ var en = {
|
|
|
334
334
|
cheats: "Cheats",
|
|
335
335
|
retroAchievements: "RetroAchievements",
|
|
336
336
|
shortcuts: "Shortcuts",
|
|
337
|
-
exit: "Exit",
|
|
337
|
+
exit: "Exit Game",
|
|
338
338
|
language: "Language",
|
|
339
|
-
selectLanguage: "Select Language"
|
|
339
|
+
selectLanguage: "Select Language",
|
|
340
|
+
haptics: "Haptic Feedback",
|
|
341
|
+
enableHaptics: "Vibration"
|
|
340
342
|
},
|
|
341
343
|
overlay: {
|
|
342
344
|
play: "PLAY",
|
|
@@ -413,13 +415,16 @@ var en = {
|
|
|
413
415
|
},
|
|
414
416
|
cheats: {
|
|
415
417
|
title: "Cheats",
|
|
416
|
-
addCheat: "Add Cheat",
|
|
417
|
-
available: "
|
|
418
|
-
emptyTitle: "No
|
|
419
|
-
emptyDesc: "
|
|
420
|
-
copy: "Copy
|
|
421
|
-
active: "
|
|
422
|
-
toggleHint: "Click
|
|
418
|
+
addCheat: "Add Manual Cheat",
|
|
419
|
+
available: "Available Cheats",
|
|
420
|
+
emptyTitle: "No Cheats Available",
|
|
421
|
+
emptyDesc: "This game does not have any known cheats.",
|
|
422
|
+
copy: "Copy",
|
|
423
|
+
active: "Active",
|
|
424
|
+
toggleHint: "Click to toggle",
|
|
425
|
+
codePlaceholder: "Enter cheat code (e.g. 00C-048-E6E)",
|
|
426
|
+
descPlaceholder: "Cheat description (optional)",
|
|
427
|
+
add: "Add Cheat"
|
|
423
428
|
},
|
|
424
429
|
saveSlots: {
|
|
425
430
|
title: "Save States",
|
|
@@ -827,7 +832,7 @@ var SaveLoadControls = React2.memo(function SaveLoadControls2({
|
|
|
827
832
|
{
|
|
828
833
|
progress: autoSaveProgress,
|
|
829
834
|
state: autoSavePaused ? "idle" : autoSaveState,
|
|
830
|
-
intervalSeconds:
|
|
835
|
+
intervalSeconds: 60,
|
|
831
836
|
isPaused: autoSavePaused,
|
|
832
837
|
onClick: onAutoSaveToggle
|
|
833
838
|
}
|
|
@@ -2592,7 +2597,8 @@ function useTouchHandlers({
|
|
|
2592
2597
|
onPress,
|
|
2593
2598
|
onPressDown,
|
|
2594
2599
|
onRelease,
|
|
2595
|
-
onPositionChange
|
|
2600
|
+
onPositionChange,
|
|
2601
|
+
hapticsEnabled = true
|
|
2596
2602
|
}) {
|
|
2597
2603
|
const isDraggingRef = React2.useRef(false);
|
|
2598
2604
|
const drag = useDrag({
|
|
@@ -2618,7 +2624,7 @@ function useTouchHandlers({
|
|
|
2618
2624
|
const touch = e.touches[0];
|
|
2619
2625
|
e.preventDefault();
|
|
2620
2626
|
e.stopPropagation();
|
|
2621
|
-
if (navigator.vibrate) {
|
|
2627
|
+
if (hapticsEnabled && navigator.vibrate) {
|
|
2622
2628
|
navigator.vibrate(8);
|
|
2623
2629
|
}
|
|
2624
2630
|
if (isSystemButton) {
|
|
@@ -2855,7 +2861,11 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2855
2861
|
customPosition,
|
|
2856
2862
|
onPositionChange,
|
|
2857
2863
|
isLandscape = false,
|
|
2858
|
-
console: console2 = ""
|
|
2864
|
+
console: console2 = "",
|
|
2865
|
+
hapticsEnabled = true,
|
|
2866
|
+
mode = "normal",
|
|
2867
|
+
isHeld = false,
|
|
2868
|
+
isInTurbo = false
|
|
2859
2869
|
}) {
|
|
2860
2870
|
const t = useKoinTranslation();
|
|
2861
2871
|
const buttonRef = React2.useRef(null);
|
|
@@ -2882,7 +2892,9 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2882
2892
|
onPress,
|
|
2883
2893
|
onPressDown,
|
|
2884
2894
|
onRelease,
|
|
2885
|
-
onPositionChange
|
|
2895
|
+
onPositionChange,
|
|
2896
|
+
hapticsEnabled
|
|
2897
|
+
// Pass haptics setting
|
|
2886
2898
|
});
|
|
2887
2899
|
useTouchEvents(buttonRef, {
|
|
2888
2900
|
onTouchStart: handleTouchStart,
|
|
@@ -2910,7 +2922,7 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2910
2922
|
width = `${config.size * 2}px`;
|
|
2911
2923
|
}
|
|
2912
2924
|
}
|
|
2913
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
2925
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2914
2926
|
"button",
|
|
2915
2927
|
{
|
|
2916
2928
|
ref: buttonRef,
|
|
@@ -2950,55 +2962,371 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2950
2962
|
},
|
|
2951
2963
|
"aria-label": label,
|
|
2952
2964
|
onContextMenu: (e) => e.preventDefault(),
|
|
2953
|
-
children:
|
|
2965
|
+
children: [
|
|
2966
|
+
(mode !== "normal" || isHeld || isInTurbo) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2967
|
+
"span",
|
|
2968
|
+
{
|
|
2969
|
+
className: "absolute -top-1 -right-1 rounded-full flex items-center justify-center",
|
|
2970
|
+
style: {
|
|
2971
|
+
width: "16px",
|
|
2972
|
+
height: "16px",
|
|
2973
|
+
backgroundColor: isHeld ? "#22c55e" : isInTurbo ? "#fbbf24" : "rgba(255,255,255,0.2)",
|
|
2974
|
+
animation: isInTurbo ? "pulse 0.5s infinite" : "none"
|
|
2975
|
+
},
|
|
2976
|
+
children: isHeld ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Hand, { size: 10, color: "white", strokeWidth: 3 }) : isInTurbo ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 10, color: "white", fill: "white" }) : mode === "hold" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Hand, { size: 10, color: "white" }) : mode === "turbo" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 10, color: "white" }) : null
|
|
2977
|
+
}
|
|
2978
|
+
),
|
|
2979
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "drop-shadow-md", children: label })
|
|
2980
|
+
]
|
|
2954
2981
|
}
|
|
2955
2982
|
);
|
|
2956
2983
|
});
|
|
2957
2984
|
var VirtualButton_default = VirtualButton;
|
|
2985
|
+
var CENTER_TOUCH_RADIUS = 0.35;
|
|
2986
|
+
var Dpad = React2__default.default.memo(function Dpad2({
|
|
2987
|
+
size = 180,
|
|
2988
|
+
x,
|
|
2989
|
+
y,
|
|
2990
|
+
containerWidth,
|
|
2991
|
+
containerHeight,
|
|
2992
|
+
systemColor = "#00FF41",
|
|
2993
|
+
isLandscape = false,
|
|
2994
|
+
customPosition,
|
|
2995
|
+
onPositionChange,
|
|
2996
|
+
hapticsEnabled = true,
|
|
2997
|
+
onButtonDown,
|
|
2998
|
+
onButtonUp
|
|
2999
|
+
}) {
|
|
3000
|
+
const dpadRef = React2.useRef(null);
|
|
3001
|
+
const activeTouchRef = React2.useRef(null);
|
|
3002
|
+
const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
3003
|
+
const upPathRef = React2.useRef(null);
|
|
3004
|
+
const downPathRef = React2.useRef(null);
|
|
3005
|
+
const leftPathRef = React2.useRef(null);
|
|
3006
|
+
const rightPathRef = React2.useRef(null);
|
|
3007
|
+
const centerCircleRef = React2.useRef(null);
|
|
3008
|
+
const displayX = customPosition ? customPosition.x : x;
|
|
3009
|
+
const displayY = customPosition ? customPosition.y : y;
|
|
3010
|
+
const releaseAllDirections = React2.useCallback(() => {
|
|
3011
|
+
activeDirectionsRef.current.forEach((dir) => onButtonUp(dir));
|
|
3012
|
+
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3013
|
+
}, [onButtonUp]);
|
|
3014
|
+
const drag = useDrag({
|
|
3015
|
+
elementSize: size,
|
|
3016
|
+
displayX,
|
|
3017
|
+
displayY,
|
|
3018
|
+
containerWidth,
|
|
3019
|
+
containerHeight,
|
|
3020
|
+
onPositionChange,
|
|
3021
|
+
centerThreshold: CENTER_TOUCH_RADIUS,
|
|
3022
|
+
onDragStart: () => {
|
|
3023
|
+
releaseAllDirections();
|
|
3024
|
+
updateVisuals(/* @__PURE__ */ new Set());
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
const getDirectionsFromTouch = React2.useCallback((touchX, touchY, rect) => {
|
|
3028
|
+
const centerX = rect.left + rect.width / 2;
|
|
3029
|
+
const centerY = rect.top + rect.height / 2;
|
|
3030
|
+
const dx = touchX - centerX;
|
|
3031
|
+
const dy = touchY - centerY;
|
|
3032
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
3033
|
+
const deadZone = rect.width / 2 * 0.15;
|
|
3034
|
+
if (distance < deadZone) return /* @__PURE__ */ new Set();
|
|
3035
|
+
const directions = /* @__PURE__ */ new Set();
|
|
3036
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
3037
|
+
if (angle >= -150 && angle <= -30) directions.add("up");
|
|
3038
|
+
if (angle >= 30 && angle <= 150) directions.add("down");
|
|
3039
|
+
if (angle >= 120 || angle <= -120) directions.add("left");
|
|
3040
|
+
if (angle >= -60 && angle <= 60) directions.add("right");
|
|
3041
|
+
return directions;
|
|
3042
|
+
}, []);
|
|
3043
|
+
const updateVisuals = React2.useCallback((directions) => {
|
|
3044
|
+
const activeFill = `${systemColor}80`;
|
|
3045
|
+
const inactiveFill = "rgba(255, 255, 255, 0.05)";
|
|
3046
|
+
const activeStroke = systemColor;
|
|
3047
|
+
const inactiveStroke = "rgba(255, 255, 255, 0.2)";
|
|
3048
|
+
const glow = `0 0 15px ${systemColor}`;
|
|
3049
|
+
const updatePart = (ref, isActive) => {
|
|
3050
|
+
if (ref.current) {
|
|
3051
|
+
ref.current.style.fill = isActive ? activeFill : inactiveFill;
|
|
3052
|
+
ref.current.style.stroke = isActive ? activeStroke : inactiveStroke;
|
|
3053
|
+
ref.current.style.filter = isActive ? `drop-shadow(${glow})` : "none";
|
|
3054
|
+
ref.current.style.transform = isActive ? "scale(0.98)" : "scale(1)";
|
|
3055
|
+
ref.current.style.transformOrigin = "center";
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
updatePart(upPathRef, directions.has("up"));
|
|
3059
|
+
updatePart(downPathRef, directions.has("down"));
|
|
3060
|
+
updatePart(leftPathRef, directions.has("left"));
|
|
3061
|
+
updatePart(rightPathRef, directions.has("right"));
|
|
3062
|
+
if (centerCircleRef.current) {
|
|
3063
|
+
const isAny = directions.size > 0;
|
|
3064
|
+
centerCircleRef.current.style.fill = isAny ? systemColor : "rgba(0,0,0,0.5)";
|
|
3065
|
+
centerCircleRef.current.style.stroke = isAny ? "#fff" : "rgba(255,255,255,0.3)";
|
|
3066
|
+
}
|
|
3067
|
+
}, [systemColor]);
|
|
3068
|
+
const updateDirections = React2.useCallback((newDirections) => {
|
|
3069
|
+
const prev = activeDirectionsRef.current;
|
|
3070
|
+
prev.forEach((dir) => {
|
|
3071
|
+
if (!newDirections.has(dir)) {
|
|
3072
|
+
onButtonUp(dir);
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
newDirections.forEach((dir) => {
|
|
3076
|
+
if (!prev.has(dir)) {
|
|
3077
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate(5);
|
|
3078
|
+
onButtonDown(dir);
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
activeDirectionsRef.current = newDirections;
|
|
3082
|
+
updateVisuals(newDirections);
|
|
3083
|
+
}, [updateVisuals, onButtonDown, onButtonUp, hapticsEnabled]);
|
|
3084
|
+
const handleTouchStart = React2.useCallback((e) => {
|
|
3085
|
+
e.preventDefault();
|
|
3086
|
+
if (activeTouchRef.current !== null) return;
|
|
3087
|
+
const touch = e.changedTouches[0];
|
|
3088
|
+
activeTouchRef.current = touch.identifier;
|
|
3089
|
+
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3090
|
+
if (!rect) return;
|
|
3091
|
+
if (onPositionChange) {
|
|
3092
|
+
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
3093
|
+
}
|
|
3094
|
+
if (!drag.isDragging) {
|
|
3095
|
+
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3096
|
+
}
|
|
3097
|
+
}, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
|
|
3098
|
+
const handleTouchMove = React2.useCallback((e) => {
|
|
3099
|
+
e.preventDefault();
|
|
3100
|
+
let touch = null;
|
|
3101
|
+
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3102
|
+
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
3103
|
+
touch = e.changedTouches[i];
|
|
3104
|
+
break;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
if (!touch) return;
|
|
3108
|
+
if (drag.isDragging) {
|
|
3109
|
+
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3110
|
+
} else if (onPositionChange) {
|
|
3111
|
+
const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
|
|
3112
|
+
if (!startedDrag) {
|
|
3113
|
+
drag.clearDragTimer();
|
|
3114
|
+
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3115
|
+
if (rect) {
|
|
3116
|
+
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
} else {
|
|
3120
|
+
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3121
|
+
if (rect) {
|
|
3122
|
+
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
|
|
3126
|
+
const handleTouchEnd = React2.useCallback((e) => {
|
|
3127
|
+
e.preventDefault();
|
|
3128
|
+
drag.clearDragTimer();
|
|
3129
|
+
let touchEnded = false;
|
|
3130
|
+
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3131
|
+
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
3132
|
+
touchEnded = true;
|
|
3133
|
+
break;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (touchEnded) {
|
|
3137
|
+
activeTouchRef.current = null;
|
|
3138
|
+
if (drag.isDragging) {
|
|
3139
|
+
drag.handleDragEnd();
|
|
3140
|
+
} else {
|
|
3141
|
+
activeDirectionsRef.current.forEach((dir) => onButtonUp(dir));
|
|
3142
|
+
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3143
|
+
updateVisuals(/* @__PURE__ */ new Set());
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}, [updateVisuals, drag, onButtonUp]);
|
|
3147
|
+
useTouchEvents(dpadRef, {
|
|
3148
|
+
onTouchStart: handleTouchStart,
|
|
3149
|
+
onTouchMove: handleTouchMove,
|
|
3150
|
+
onTouchEnd: handleTouchEnd,
|
|
3151
|
+
onTouchCancel: handleTouchEnd
|
|
3152
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3153
|
+
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3154
|
+
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3155
|
+
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
3156
|
+
const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
|
|
3157
|
+
const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
|
|
3158
|
+
const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
|
|
3159
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3160
|
+
"div",
|
|
3161
|
+
{
|
|
3162
|
+
ref: dpadRef,
|
|
3163
|
+
className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
|
|
3164
|
+
style: {
|
|
3165
|
+
top: 0,
|
|
3166
|
+
left: 0,
|
|
3167
|
+
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
|
|
3168
|
+
width: size,
|
|
3169
|
+
height: size,
|
|
3170
|
+
opacity: isLandscape ? 0.75 : 0.9,
|
|
3171
|
+
WebkitTouchCallout: "none",
|
|
3172
|
+
WebkitUserSelect: "none",
|
|
3173
|
+
touchAction: "none",
|
|
3174
|
+
transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
|
|
3175
|
+
},
|
|
3176
|
+
children: [
|
|
3177
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${drag.isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3178
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
|
|
3179
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3180
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3181
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: downPathRef, d: dDown, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3182
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: leftPathRef, d: dLeft, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3183
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3184
|
+
"circle",
|
|
3185
|
+
{
|
|
3186
|
+
ref: centerCircleRef,
|
|
3187
|
+
cx: "50",
|
|
3188
|
+
cy: "50",
|
|
3189
|
+
r: "12",
|
|
3190
|
+
fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3191
|
+
stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3192
|
+
strokeWidth: drag.isDragging ? 2 : 1
|
|
3193
|
+
}
|
|
3194
|
+
),
|
|
3195
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3196
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,85 L 50,75 M 45,80 L 50,85 L 55,80", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3197
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 15,50 L 25,50 M 20,45 L 15,50 L 20,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3198
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 85,50 L 75,50 M 80,45 L 85,50 L 80,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" })
|
|
3199
|
+
] })
|
|
3200
|
+
]
|
|
3201
|
+
}
|
|
3202
|
+
);
|
|
3203
|
+
});
|
|
3204
|
+
var Dpad_default = Dpad;
|
|
2958
3205
|
|
|
2959
|
-
// src/
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3206
|
+
// src/components/VirtualController/positioning.ts
|
|
3207
|
+
function adjustButtonPosition(config, context) {
|
|
3208
|
+
const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
|
|
3209
|
+
return {
|
|
3210
|
+
...config,
|
|
3211
|
+
size: Math.floor(config.size * sizeMultiplier)
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
var STORAGE_KEY = "virtual-button-positions";
|
|
3215
|
+
function useButtonPositions() {
|
|
3216
|
+
const [landscapePositions, setLandscapePositions] = React2.useState({});
|
|
3217
|
+
const [portraitPositions, setPortraitPositions] = React2.useState({});
|
|
3218
|
+
React2.useEffect(() => {
|
|
3219
|
+
try {
|
|
3220
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
3221
|
+
if (stored) {
|
|
3222
|
+
const parsed = JSON.parse(stored);
|
|
3223
|
+
if (parsed.landscape && parsed.portrait) {
|
|
3224
|
+
setLandscapePositions(parsed.landscape);
|
|
3225
|
+
setPortraitPositions(parsed.portrait);
|
|
3226
|
+
} else {
|
|
3227
|
+
setLandscapePositions(parsed);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
} catch (e) {
|
|
3231
|
+
console.error("Failed to load button positions:", e);
|
|
3232
|
+
}
|
|
3233
|
+
}, []);
|
|
3234
|
+
const savePosition = React2.useCallback((buttonType, x, y, isLandscape) => {
|
|
3235
|
+
if (isLandscape) {
|
|
3236
|
+
setLandscapePositions((prev) => {
|
|
3237
|
+
const updated = { ...prev, [buttonType]: { x, y } };
|
|
3238
|
+
try {
|
|
3239
|
+
const stored = {
|
|
3240
|
+
landscape: updated,
|
|
3241
|
+
portrait: portraitPositions
|
|
3242
|
+
};
|
|
3243
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
3244
|
+
} catch (e) {
|
|
3245
|
+
console.error("Failed to save button position:", e);
|
|
3246
|
+
}
|
|
3247
|
+
return updated;
|
|
3248
|
+
});
|
|
3249
|
+
} else {
|
|
3250
|
+
setPortraitPositions((prev) => {
|
|
3251
|
+
const updated = { ...prev, [buttonType]: { x, y } };
|
|
3252
|
+
try {
|
|
3253
|
+
const stored = {
|
|
3254
|
+
landscape: landscapePositions,
|
|
3255
|
+
portrait: updated
|
|
3256
|
+
};
|
|
3257
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
3258
|
+
} catch (e) {
|
|
3259
|
+
console.error("Failed to save button position:", e);
|
|
3260
|
+
}
|
|
3261
|
+
return updated;
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
}, [landscapePositions, portraitPositions]);
|
|
3265
|
+
const getPosition = React2.useCallback((buttonType, isLandscape) => {
|
|
3266
|
+
const positions = isLandscape ? landscapePositions : portraitPositions;
|
|
3267
|
+
return positions[buttonType] || null;
|
|
3268
|
+
}, [landscapePositions, portraitPositions]);
|
|
3269
|
+
const resetPositions = React2.useCallback(() => {
|
|
3270
|
+
setLandscapePositions({});
|
|
3271
|
+
setPortraitPositions({});
|
|
3272
|
+
try {
|
|
3273
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
3274
|
+
} catch (e) {
|
|
3275
|
+
console.error("Failed to reset button positions:", e);
|
|
3276
|
+
}
|
|
3277
|
+
}, []);
|
|
3278
|
+
return {
|
|
3279
|
+
landscapePositions,
|
|
3280
|
+
portraitPositions,
|
|
3281
|
+
savePosition,
|
|
3282
|
+
getPosition,
|
|
3283
|
+
resetPositions
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// src/lib/controls/types.ts
|
|
3288
|
+
var DPAD_BUTTONS2 = ["up", "down", "left", "right"];
|
|
3289
|
+
var FACE_BUTTONS = ["a", "b", "x", "y"];
|
|
3290
|
+
var SHOULDER_BUTTONS = ["l", "r"];
|
|
3291
|
+
var TRIGGER_BUTTONS = ["l2", "r2"];
|
|
3292
|
+
var STICK_BUTTONS = ["l3", "r3"];
|
|
3293
|
+
var SYSTEM_BUTTONS = ["start", "select"];
|
|
3294
|
+
var ALL_BUTTONS = [
|
|
3295
|
+
...DPAD_BUTTONS2,
|
|
3296
|
+
...FACE_BUTTONS,
|
|
3297
|
+
...SHOULDER_BUTTONS,
|
|
3298
|
+
...TRIGGER_BUTTONS,
|
|
3299
|
+
...STICK_BUTTONS,
|
|
3300
|
+
...SYSTEM_BUTTONS
|
|
3301
|
+
];
|
|
3302
|
+
|
|
3303
|
+
// src/lib/controls/defaults.ts
|
|
3304
|
+
var STANDARD_GAMEPAD_BUTTONS = {
|
|
3305
|
+
// Face buttons (Xbox/PlayStation layout)
|
|
3306
|
+
a: 0,
|
|
3307
|
+
// A / Cross (bottom)
|
|
3308
|
+
b: 1,
|
|
3309
|
+
// B / Circle (right)
|
|
3310
|
+
x: 2,
|
|
3311
|
+
// X / Square (left)
|
|
3312
|
+
y: 3,
|
|
3313
|
+
// Y / Triangle (top)
|
|
3314
|
+
// Shoulders
|
|
3315
|
+
l: 4,
|
|
3316
|
+
// LB / L1
|
|
3317
|
+
r: 5,
|
|
3318
|
+
// RB / R1
|
|
3319
|
+
// Triggers
|
|
3320
|
+
l2: 6,
|
|
3321
|
+
// LT / L2
|
|
3322
|
+
r2: 7,
|
|
3323
|
+
// RT / R2
|
|
3324
|
+
// System
|
|
3325
|
+
select: 8,
|
|
3326
|
+
// Back / Select / Share
|
|
3327
|
+
start: 9,
|
|
3328
|
+
// Start / Options
|
|
3329
|
+
// Stick clicks
|
|
3002
3330
|
l3: 10,
|
|
3003
3331
|
// Left stick click
|
|
3004
3332
|
r3: 11,
|
|
@@ -3541,343 +3869,6 @@ function getKeyboardCode(buttonType, controls) {
|
|
|
3541
3869
|
}
|
|
3542
3870
|
return DEFAULT_CONTROLS[key] ?? null;
|
|
3543
3871
|
}
|
|
3544
|
-
function getKeyName(code) {
|
|
3545
|
-
if (code.startsWith("Key")) return code.slice(3).toLowerCase();
|
|
3546
|
-
if (code.startsWith("Arrow")) return code.slice(5);
|
|
3547
|
-
if (code === "Enter") return "Enter";
|
|
3548
|
-
if (code === "ShiftRight" || code === "ShiftLeft") return "Shift";
|
|
3549
|
-
return code;
|
|
3550
|
-
}
|
|
3551
|
-
function getCanvas() {
|
|
3552
|
-
return document.querySelector(".game-canvas-container canvas") || document.querySelector("canvas");
|
|
3553
|
-
}
|
|
3554
|
-
function dispatchKeyboardEvent(type, code) {
|
|
3555
|
-
const canvas = getCanvas();
|
|
3556
|
-
if (!canvas) {
|
|
3557
|
-
return false;
|
|
3558
|
-
}
|
|
3559
|
-
const event = new KeyboardEvent(type, {
|
|
3560
|
-
code,
|
|
3561
|
-
key: getKeyName(code),
|
|
3562
|
-
bubbles: true,
|
|
3563
|
-
cancelable: true
|
|
3564
|
-
});
|
|
3565
|
-
canvas.dispatchEvent(event);
|
|
3566
|
-
return true;
|
|
3567
|
-
}
|
|
3568
|
-
var CENTER_TOUCH_RADIUS = 0.35;
|
|
3569
|
-
var Dpad = React2__default.default.memo(function Dpad2({
|
|
3570
|
-
size = 180,
|
|
3571
|
-
x,
|
|
3572
|
-
y,
|
|
3573
|
-
containerWidth,
|
|
3574
|
-
containerHeight,
|
|
3575
|
-
controls,
|
|
3576
|
-
systemColor = "#00FF41",
|
|
3577
|
-
isLandscape = false,
|
|
3578
|
-
customPosition,
|
|
3579
|
-
onPositionChange
|
|
3580
|
-
}) {
|
|
3581
|
-
const dpadRef = React2.useRef(null);
|
|
3582
|
-
const activeTouchRef = React2.useRef(null);
|
|
3583
|
-
const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
3584
|
-
const upPathRef = React2.useRef(null);
|
|
3585
|
-
const downPathRef = React2.useRef(null);
|
|
3586
|
-
const leftPathRef = React2.useRef(null);
|
|
3587
|
-
const rightPathRef = React2.useRef(null);
|
|
3588
|
-
const centerCircleRef = React2.useRef(null);
|
|
3589
|
-
const displayX = customPosition ? customPosition.x : x;
|
|
3590
|
-
const displayY = customPosition ? customPosition.y : y;
|
|
3591
|
-
const releaseAllDirections = React2.useCallback((getKeyCode2) => {
|
|
3592
|
-
activeDirectionsRef.current.forEach((dir) => {
|
|
3593
|
-
const keyCode = getKeyCode2(dir);
|
|
3594
|
-
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3595
|
-
});
|
|
3596
|
-
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3597
|
-
}, []);
|
|
3598
|
-
const getKeyCode = React2.useCallback((direction) => {
|
|
3599
|
-
if (!controls) {
|
|
3600
|
-
const defaults = {
|
|
3601
|
-
up: "ArrowUp",
|
|
3602
|
-
down: "ArrowDown",
|
|
3603
|
-
left: "ArrowLeft",
|
|
3604
|
-
right: "ArrowRight"
|
|
3605
|
-
};
|
|
3606
|
-
return defaults[direction];
|
|
3607
|
-
}
|
|
3608
|
-
return controls[direction] || "";
|
|
3609
|
-
}, [controls]);
|
|
3610
|
-
const drag = useDrag({
|
|
3611
|
-
elementSize: size,
|
|
3612
|
-
displayX,
|
|
3613
|
-
displayY,
|
|
3614
|
-
containerWidth,
|
|
3615
|
-
containerHeight,
|
|
3616
|
-
onPositionChange,
|
|
3617
|
-
centerThreshold: CENTER_TOUCH_RADIUS,
|
|
3618
|
-
onDragStart: () => {
|
|
3619
|
-
releaseAllDirections(getKeyCode);
|
|
3620
|
-
updateVisuals(/* @__PURE__ */ new Set());
|
|
3621
|
-
}
|
|
3622
|
-
});
|
|
3623
|
-
const getDirectionsFromTouch = React2.useCallback((touchX, touchY, rect) => {
|
|
3624
|
-
const centerX = rect.left + rect.width / 2;
|
|
3625
|
-
const centerY = rect.top + rect.height / 2;
|
|
3626
|
-
const dx = touchX - centerX;
|
|
3627
|
-
const dy = touchY - centerY;
|
|
3628
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
3629
|
-
const deadZone = rect.width / 2 * 0.15;
|
|
3630
|
-
if (distance < deadZone) return /* @__PURE__ */ new Set();
|
|
3631
|
-
const directions = /* @__PURE__ */ new Set();
|
|
3632
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
3633
|
-
if (angle >= -150 && angle <= -30) directions.add("up");
|
|
3634
|
-
if (angle >= 30 && angle <= 150) directions.add("down");
|
|
3635
|
-
if (angle >= 120 || angle <= -120) directions.add("left");
|
|
3636
|
-
if (angle >= -60 && angle <= 60) directions.add("right");
|
|
3637
|
-
return directions;
|
|
3638
|
-
}, []);
|
|
3639
|
-
const updateVisuals = React2.useCallback((directions) => {
|
|
3640
|
-
const activeFill = `${systemColor}80`;
|
|
3641
|
-
const inactiveFill = "rgba(255, 255, 255, 0.05)";
|
|
3642
|
-
const activeStroke = systemColor;
|
|
3643
|
-
const inactiveStroke = "rgba(255, 255, 255, 0.2)";
|
|
3644
|
-
const glow = `0 0 15px ${systemColor}`;
|
|
3645
|
-
const updatePart = (ref, isActive) => {
|
|
3646
|
-
if (ref.current) {
|
|
3647
|
-
ref.current.style.fill = isActive ? activeFill : inactiveFill;
|
|
3648
|
-
ref.current.style.stroke = isActive ? activeStroke : inactiveStroke;
|
|
3649
|
-
ref.current.style.filter = isActive ? `drop-shadow(${glow})` : "none";
|
|
3650
|
-
ref.current.style.transform = isActive ? "scale(0.98)" : "scale(1)";
|
|
3651
|
-
ref.current.style.transformOrigin = "center";
|
|
3652
|
-
}
|
|
3653
|
-
};
|
|
3654
|
-
updatePart(upPathRef, directions.has("up"));
|
|
3655
|
-
updatePart(downPathRef, directions.has("down"));
|
|
3656
|
-
updatePart(leftPathRef, directions.has("left"));
|
|
3657
|
-
updatePart(rightPathRef, directions.has("right"));
|
|
3658
|
-
if (centerCircleRef.current) {
|
|
3659
|
-
const isAny = directions.size > 0;
|
|
3660
|
-
centerCircleRef.current.style.fill = isAny ? systemColor : "rgba(0,0,0,0.5)";
|
|
3661
|
-
centerCircleRef.current.style.stroke = isAny ? "#fff" : "rgba(255,255,255,0.3)";
|
|
3662
|
-
}
|
|
3663
|
-
}, [systemColor]);
|
|
3664
|
-
const updateDirections = React2.useCallback((newDirections) => {
|
|
3665
|
-
const prev = activeDirectionsRef.current;
|
|
3666
|
-
prev.forEach((dir) => {
|
|
3667
|
-
if (!newDirections.has(dir)) {
|
|
3668
|
-
const keyCode = getKeyCode(dir);
|
|
3669
|
-
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3670
|
-
}
|
|
3671
|
-
});
|
|
3672
|
-
newDirections.forEach((dir) => {
|
|
3673
|
-
if (!prev.has(dir)) {
|
|
3674
|
-
const keyCode = getKeyCode(dir);
|
|
3675
|
-
if (keyCode) {
|
|
3676
|
-
if (navigator.vibrate) navigator.vibrate(5);
|
|
3677
|
-
dispatchKeyboardEvent("keydown", keyCode);
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
});
|
|
3681
|
-
activeDirectionsRef.current = newDirections;
|
|
3682
|
-
updateVisuals(newDirections);
|
|
3683
|
-
}, [getKeyCode, updateVisuals]);
|
|
3684
|
-
const handleTouchStart = React2.useCallback((e) => {
|
|
3685
|
-
e.preventDefault();
|
|
3686
|
-
if (activeTouchRef.current !== null) return;
|
|
3687
|
-
const touch = e.changedTouches[0];
|
|
3688
|
-
activeTouchRef.current = touch.identifier;
|
|
3689
|
-
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3690
|
-
if (!rect) return;
|
|
3691
|
-
if (onPositionChange) {
|
|
3692
|
-
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
3693
|
-
}
|
|
3694
|
-
if (!drag.isDragging) {
|
|
3695
|
-
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3696
|
-
}
|
|
3697
|
-
}, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
|
|
3698
|
-
const handleTouchMove = React2.useCallback((e) => {
|
|
3699
|
-
e.preventDefault();
|
|
3700
|
-
let touch = null;
|
|
3701
|
-
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3702
|
-
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
3703
|
-
touch = e.changedTouches[i];
|
|
3704
|
-
break;
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
if (!touch) return;
|
|
3708
|
-
if (drag.isDragging) {
|
|
3709
|
-
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3710
|
-
} else {
|
|
3711
|
-
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3712
|
-
if (rect) {
|
|
3713
|
-
drag.clearDragTimer();
|
|
3714
|
-
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
|
-
}, [drag, getDirectionsFromTouch, updateDirections]);
|
|
3718
|
-
const handleTouchEnd = React2.useCallback((e) => {
|
|
3719
|
-
e.preventDefault();
|
|
3720
|
-
drag.clearDragTimer();
|
|
3721
|
-
let touchEnded = false;
|
|
3722
|
-
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3723
|
-
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
3724
|
-
touchEnded = true;
|
|
3725
|
-
break;
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
|
-
if (touchEnded) {
|
|
3729
|
-
activeTouchRef.current = null;
|
|
3730
|
-
if (drag.isDragging) {
|
|
3731
|
-
drag.handleDragEnd();
|
|
3732
|
-
} else {
|
|
3733
|
-
activeDirectionsRef.current.forEach((dir) => {
|
|
3734
|
-
const keyCode = getKeyCode(dir);
|
|
3735
|
-
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3736
|
-
});
|
|
3737
|
-
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3738
|
-
updateVisuals(/* @__PURE__ */ new Set());
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
}, [getKeyCode, updateVisuals, drag]);
|
|
3742
|
-
useTouchEvents(dpadRef, {
|
|
3743
|
-
onTouchStart: handleTouchStart,
|
|
3744
|
-
onTouchMove: handleTouchMove,
|
|
3745
|
-
onTouchEnd: handleTouchEnd,
|
|
3746
|
-
onTouchCancel: handleTouchEnd
|
|
3747
|
-
}, { cleanup: drag.clearDragTimer });
|
|
3748
|
-
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3749
|
-
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3750
|
-
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
3751
|
-
const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
|
|
3752
|
-
const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
|
|
3753
|
-
const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
|
|
3754
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3755
|
-
"div",
|
|
3756
|
-
{
|
|
3757
|
-
ref: dpadRef,
|
|
3758
|
-
className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
|
|
3759
|
-
style: {
|
|
3760
|
-
top: 0,
|
|
3761
|
-
left: 0,
|
|
3762
|
-
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
|
|
3763
|
-
width: size,
|
|
3764
|
-
height: size,
|
|
3765
|
-
opacity: isLandscape ? 0.75 : 0.9,
|
|
3766
|
-
WebkitTouchCallout: "none",
|
|
3767
|
-
WebkitUserSelect: "none",
|
|
3768
|
-
touchAction: "none",
|
|
3769
|
-
transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
|
|
3770
|
-
},
|
|
3771
|
-
children: [
|
|
3772
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${drag.isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3773
|
-
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
|
|
3774
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3775
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3776
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: downPathRef, d: dDown, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3777
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: leftPathRef, d: dLeft, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3778
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3779
|
-
"circle",
|
|
3780
|
-
{
|
|
3781
|
-
ref: centerCircleRef,
|
|
3782
|
-
cx: "50",
|
|
3783
|
-
cy: "50",
|
|
3784
|
-
r: "12",
|
|
3785
|
-
fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3786
|
-
stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3787
|
-
strokeWidth: drag.isDragging ? 2 : 1
|
|
3788
|
-
}
|
|
3789
|
-
),
|
|
3790
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3791
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,85 L 50,75 M 45,80 L 50,85 L 55,80", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3792
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 15,50 L 25,50 M 20,45 L 15,50 L 20,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
3793
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 85,50 L 75,50 M 80,45 L 85,50 L 80,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" })
|
|
3794
|
-
] })
|
|
3795
|
-
]
|
|
3796
|
-
}
|
|
3797
|
-
);
|
|
3798
|
-
});
|
|
3799
|
-
var Dpad_default = Dpad;
|
|
3800
|
-
|
|
3801
|
-
// src/components/VirtualController/positioning.ts
|
|
3802
|
-
function adjustButtonPosition(config, context) {
|
|
3803
|
-
const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
|
|
3804
|
-
return {
|
|
3805
|
-
...config,
|
|
3806
|
-
size: Math.floor(config.size * sizeMultiplier)
|
|
3807
|
-
};
|
|
3808
|
-
}
|
|
3809
|
-
var STORAGE_KEY = "virtual-button-positions";
|
|
3810
|
-
function useButtonPositions() {
|
|
3811
|
-
const [landscapePositions, setLandscapePositions] = React2.useState({});
|
|
3812
|
-
const [portraitPositions, setPortraitPositions] = React2.useState({});
|
|
3813
|
-
React2.useEffect(() => {
|
|
3814
|
-
try {
|
|
3815
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
3816
|
-
if (stored) {
|
|
3817
|
-
const parsed = JSON.parse(stored);
|
|
3818
|
-
if (parsed.landscape && parsed.portrait) {
|
|
3819
|
-
setLandscapePositions(parsed.landscape);
|
|
3820
|
-
setPortraitPositions(parsed.portrait);
|
|
3821
|
-
} else {
|
|
3822
|
-
setLandscapePositions(parsed);
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
} catch (e) {
|
|
3826
|
-
console.error("Failed to load button positions:", e);
|
|
3827
|
-
}
|
|
3828
|
-
}, []);
|
|
3829
|
-
const savePosition = React2.useCallback((buttonType, x, y, isLandscape) => {
|
|
3830
|
-
if (isLandscape) {
|
|
3831
|
-
setLandscapePositions((prev) => {
|
|
3832
|
-
const updated = { ...prev, [buttonType]: { x, y } };
|
|
3833
|
-
try {
|
|
3834
|
-
const stored = {
|
|
3835
|
-
landscape: updated,
|
|
3836
|
-
portrait: portraitPositions
|
|
3837
|
-
};
|
|
3838
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
3839
|
-
} catch (e) {
|
|
3840
|
-
console.error("Failed to save button position:", e);
|
|
3841
|
-
}
|
|
3842
|
-
return updated;
|
|
3843
|
-
});
|
|
3844
|
-
} else {
|
|
3845
|
-
setPortraitPositions((prev) => {
|
|
3846
|
-
const updated = { ...prev, [buttonType]: { x, y } };
|
|
3847
|
-
try {
|
|
3848
|
-
const stored = {
|
|
3849
|
-
landscape: landscapePositions,
|
|
3850
|
-
portrait: updated
|
|
3851
|
-
};
|
|
3852
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
3853
|
-
} catch (e) {
|
|
3854
|
-
console.error("Failed to save button position:", e);
|
|
3855
|
-
}
|
|
3856
|
-
return updated;
|
|
3857
|
-
});
|
|
3858
|
-
}
|
|
3859
|
-
}, [landscapePositions, portraitPositions]);
|
|
3860
|
-
const getPosition = React2.useCallback((buttonType, isLandscape) => {
|
|
3861
|
-
const positions = isLandscape ? landscapePositions : portraitPositions;
|
|
3862
|
-
return positions[buttonType] || null;
|
|
3863
|
-
}, [landscapePositions, portraitPositions]);
|
|
3864
|
-
const resetPositions = React2.useCallback(() => {
|
|
3865
|
-
setLandscapePositions({});
|
|
3866
|
-
setPortraitPositions({});
|
|
3867
|
-
try {
|
|
3868
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
3869
|
-
} catch (e) {
|
|
3870
|
-
console.error("Failed to reset button positions:", e);
|
|
3871
|
-
}
|
|
3872
|
-
}, []);
|
|
3873
|
-
return {
|
|
3874
|
-
landscapePositions,
|
|
3875
|
-
portraitPositions,
|
|
3876
|
-
savePosition,
|
|
3877
|
-
getPosition,
|
|
3878
|
-
resetPositions
|
|
3879
|
-
};
|
|
3880
|
-
}
|
|
3881
3872
|
var STORAGE_KEY2 = "koin-controls-hint-shown";
|
|
3882
3873
|
function ControlsHint({ isVisible }) {
|
|
3883
3874
|
const [show, setShow] = React2.useState(false);
|
|
@@ -3915,8 +3906,16 @@ function ControlsHint({ isVisible }) {
|
|
|
3915
3906
|
children: [
|
|
3916
3907
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 32, className: "text-green-400" }) }) }),
|
|
3917
3908
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
|
|
3918
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-
|
|
3919
|
-
|
|
3909
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3910
|
+
"Use the ",
|
|
3911
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { size: 12, className: "inline mx-1 text-white" }),
|
|
3912
|
+
" ",
|
|
3913
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "lock icon" }),
|
|
3914
|
+
" at the top to unlock controls for repositioning."
|
|
3915
|
+
] }),
|
|
3916
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3917
|
+
"When unlocked, ",
|
|
3918
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "long-press" }),
|
|
3920
3919
|
" any button or the ",
|
|
3921
3920
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "D-pad center" }),
|
|
3922
3921
|
" to drag and reposition it."
|
|
@@ -3954,7 +3953,7 @@ function LockButton({
|
|
|
3954
3953
|
"button",
|
|
3955
3954
|
{
|
|
3956
3955
|
onClick: onToggle,
|
|
3957
|
-
className: "
|
|
3956
|
+
className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
|
|
3958
3957
|
style: {
|
|
3959
3958
|
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3960
3959
|
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
@@ -3968,18 +3967,164 @@ function LockButton({
|
|
|
3968
3967
|
}
|
|
3969
3968
|
)
|
|
3970
3969
|
}
|
|
3971
|
-
);
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
function HoldButton({
|
|
3973
|
+
isActive,
|
|
3974
|
+
onToggle,
|
|
3975
|
+
systemColor = "#00FF41"
|
|
3976
|
+
}) {
|
|
3977
|
+
const Icon = isActive ? lucideReact.StopCircle : lucideReact.Hand;
|
|
3978
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3979
|
+
"button",
|
|
3980
|
+
{
|
|
3981
|
+
onClick: onToggle,
|
|
3982
|
+
className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
|
|
3983
|
+
style: {
|
|
3984
|
+
backgroundColor: isActive ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3985
|
+
border: `1px solid ${isActive ? "rgba(255,255,255,0.4)" : systemColor}`
|
|
3986
|
+
},
|
|
3987
|
+
"aria-label": isActive ? "Disable Hold Mode" : "Enable Button Hold Mode",
|
|
3988
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3989
|
+
Icon,
|
|
3990
|
+
{
|
|
3991
|
+
size: 18,
|
|
3992
|
+
style: { color: isActive ? "#fff" : systemColor }
|
|
3993
|
+
}
|
|
3994
|
+
)
|
|
3995
|
+
}
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
function TurboButton({
|
|
3999
|
+
isActive,
|
|
4000
|
+
onToggle,
|
|
4001
|
+
systemColor = "#00FF41"
|
|
4002
|
+
}) {
|
|
4003
|
+
const Icon = isActive ? lucideReact.ZapOff : lucideReact.Zap;
|
|
4004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4005
|
+
"button",
|
|
4006
|
+
{
|
|
4007
|
+
onClick: onToggle,
|
|
4008
|
+
className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
|
|
4009
|
+
style: {
|
|
4010
|
+
backgroundColor: isActive ? "rgba(255,200,0,0.3)" : `${systemColor}20`,
|
|
4011
|
+
border: `1px solid ${isActive ? "rgba(255,200,0,0.6)" : systemColor}`
|
|
4012
|
+
},
|
|
4013
|
+
"aria-label": isActive ? "Disable Turbo Mode" : "Enable Turbo Fire Mode",
|
|
4014
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4015
|
+
Icon,
|
|
4016
|
+
{
|
|
4017
|
+
size: 18,
|
|
4018
|
+
style: { color: isActive ? "#FFC800" : systemColor }
|
|
4019
|
+
}
|
|
4020
|
+
)
|
|
4021
|
+
}
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
var MODE_CONFIG = {
|
|
4025
|
+
hold: {
|
|
4026
|
+
Icon: lucideReact.Hand,
|
|
4027
|
+
title: "Hold Mode",
|
|
4028
|
+
instruction: "Tap a button to hold it",
|
|
4029
|
+
buttonIcon: lucideReact.Hand,
|
|
4030
|
+
buttonColor: "#22c55e"
|
|
4031
|
+
// green
|
|
4032
|
+
},
|
|
4033
|
+
turbo: {
|
|
4034
|
+
Icon: lucideReact.Zap,
|
|
4035
|
+
title: "Turbo Mode",
|
|
4036
|
+
instruction: "Tap a button for rapid fire",
|
|
4037
|
+
buttonIcon: lucideReact.Zap,
|
|
4038
|
+
buttonColor: "#fbbf24"
|
|
4039
|
+
// yellow
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
4042
|
+
function ModeOverlay({
|
|
4043
|
+
mode,
|
|
4044
|
+
heldButtons,
|
|
4045
|
+
turboButtons,
|
|
4046
|
+
systemColor = "#00FF41",
|
|
4047
|
+
onExit
|
|
4048
|
+
}) {
|
|
4049
|
+
if (!mode) return null;
|
|
4050
|
+
const config = MODE_CONFIG[mode];
|
|
4051
|
+
const buttons = mode === "hold" ? heldButtons : turboButtons;
|
|
4052
|
+
const buttonArray = Array.from(buttons);
|
|
4053
|
+
const { Icon, buttonIcon: ButtonIcon } = config;
|
|
4054
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed top-0 left-0 right-0 z-40 flex justify-center pt-4 pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4055
|
+
"div",
|
|
4056
|
+
{
|
|
4057
|
+
className: "relative px-5 py-3 rounded-2xl backdrop-blur-md border pointer-events-auto",
|
|
4058
|
+
style: {
|
|
4059
|
+
backgroundColor: "rgba(0,0,0,0.85)",
|
|
4060
|
+
borderColor: `${systemColor}60`,
|
|
4061
|
+
boxShadow: `0 4px 20px ${systemColor}30`
|
|
4062
|
+
},
|
|
4063
|
+
children: [
|
|
4064
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4065
|
+
"button",
|
|
4066
|
+
{
|
|
4067
|
+
onClick: onExit,
|
|
4068
|
+
className: "absolute -top-2 -right-2 w-7 h-7 rounded-full flex items-center justify-center transition-transform active:scale-90",
|
|
4069
|
+
style: {
|
|
4070
|
+
backgroundColor: "#ef4444",
|
|
4071
|
+
border: "2px solid rgba(255,255,255,0.3)"
|
|
4072
|
+
},
|
|
4073
|
+
"aria-label": "Exit mode",
|
|
4074
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, color: "white", strokeWidth: 3 })
|
|
4075
|
+
}
|
|
4076
|
+
),
|
|
4077
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4078
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4079
|
+
"div",
|
|
4080
|
+
{
|
|
4081
|
+
className: "w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0",
|
|
4082
|
+
style: { backgroundColor: `${systemColor}30` },
|
|
4083
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 20, style: { color: systemColor } })
|
|
4084
|
+
}
|
|
4085
|
+
),
|
|
4086
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-left", children: [
|
|
4087
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-white font-bold text-sm", children: config.title }),
|
|
4088
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-white/60 text-xs", children: config.instruction })
|
|
4089
|
+
] })
|
|
4090
|
+
] }),
|
|
4091
|
+
buttonArray.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 justify-center mt-2 pt-2 border-t border-white/10", children: buttonArray.map((button) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4092
|
+
"span",
|
|
4093
|
+
{
|
|
4094
|
+
className: "px-2 py-0.5 rounded-full text-[10px] font-bold uppercase flex items-center gap-1",
|
|
4095
|
+
style: {
|
|
4096
|
+
backgroundColor: `${config.buttonColor}25`,
|
|
4097
|
+
color: config.buttonColor
|
|
4098
|
+
},
|
|
4099
|
+
children: [
|
|
4100
|
+
/* @__PURE__ */ jsxRuntime.jsx(ButtonIcon, { size: 10 }),
|
|
4101
|
+
button
|
|
4102
|
+
]
|
|
4103
|
+
},
|
|
4104
|
+
button
|
|
4105
|
+
)) })
|
|
4106
|
+
]
|
|
4107
|
+
}
|
|
4108
|
+
) });
|
|
3972
4109
|
}
|
|
3973
4110
|
var LOCK_KEY = "koin-controls-locked";
|
|
3974
4111
|
function VirtualController({
|
|
3975
4112
|
system,
|
|
3976
4113
|
isRunning,
|
|
3977
4114
|
controls,
|
|
3978
|
-
systemColor = "#00FF41"
|
|
4115
|
+
systemColor = "#00FF41",
|
|
3979
4116
|
// Default retro green
|
|
4117
|
+
hapticsEnabled = true,
|
|
4118
|
+
onButtonDown,
|
|
4119
|
+
onButtonUp,
|
|
4120
|
+
onPause,
|
|
4121
|
+
onResume
|
|
3980
4122
|
}) {
|
|
3981
4123
|
const { isMobile, isLandscape, isPortrait } = useMobile();
|
|
3982
4124
|
const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
4125
|
+
const pressedButtonsRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
4126
|
+
const [heldButtons, setHeldButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
4127
|
+
const [isHoldMode, setIsHoldMode] = React2.useState(false);
|
|
3983
4128
|
const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
|
|
3984
4129
|
const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
|
|
3985
4130
|
const [isLocked, setIsLocked] = React2.useState(true);
|
|
@@ -3997,6 +4142,28 @@ function VirtualController({
|
|
|
3997
4142
|
return newValue;
|
|
3998
4143
|
});
|
|
3999
4144
|
}, []);
|
|
4145
|
+
const toggleHoldMode = React2.useCallback(() => {
|
|
4146
|
+
setIsHoldMode((prev) => {
|
|
4147
|
+
if (prev) {
|
|
4148
|
+
onResume();
|
|
4149
|
+
} else {
|
|
4150
|
+
onPause();
|
|
4151
|
+
}
|
|
4152
|
+
return !prev;
|
|
4153
|
+
});
|
|
4154
|
+
}, [onPause, onResume]);
|
|
4155
|
+
const [isTurboMode, setIsTurboMode] = React2.useState(false);
|
|
4156
|
+
const [turboButtons, setTurboButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
4157
|
+
const toggleTurboMode = React2.useCallback(() => {
|
|
4158
|
+
setIsTurboMode((prev) => {
|
|
4159
|
+
if (prev) {
|
|
4160
|
+
onResume();
|
|
4161
|
+
} else {
|
|
4162
|
+
onPause();
|
|
4163
|
+
}
|
|
4164
|
+
return !prev;
|
|
4165
|
+
});
|
|
4166
|
+
}, [onPause, onResume]);
|
|
4000
4167
|
const layout = getLayoutForSystem(system);
|
|
4001
4168
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
4002
4169
|
if (isPortrait) {
|
|
@@ -4066,9 +4233,9 @@ function VirtualController({
|
|
|
4066
4233
|
return;
|
|
4067
4234
|
}
|
|
4068
4235
|
setPressedButtons((prev) => new Set(prev).add(buttonType));
|
|
4069
|
-
|
|
4236
|
+
onButtonDown(buttonType);
|
|
4070
4237
|
setTimeout(() => {
|
|
4071
|
-
|
|
4238
|
+
onButtonUp(buttonType);
|
|
4072
4239
|
setPressedButtons((prev) => {
|
|
4073
4240
|
const next = new Set(prev);
|
|
4074
4241
|
next.delete(buttonType);
|
|
@@ -4076,7 +4243,7 @@ function VirtualController({
|
|
|
4076
4243
|
});
|
|
4077
4244
|
}, 100);
|
|
4078
4245
|
},
|
|
4079
|
-
[isRunning, getButtonKeyboardCode]
|
|
4246
|
+
[isRunning, getButtonKeyboardCode, onButtonDown, onButtonUp]
|
|
4080
4247
|
);
|
|
4081
4248
|
const handlePressDown = React2.useCallback(
|
|
4082
4249
|
(buttonType) => {
|
|
@@ -4085,15 +4252,70 @@ function VirtualController({
|
|
|
4085
4252
|
if (isSystemButton) return;
|
|
4086
4253
|
const keyboardCode = getButtonKeyboardCode(buttonType);
|
|
4087
4254
|
if (!keyboardCode) return;
|
|
4255
|
+
if (isHoldMode) {
|
|
4256
|
+
const isHeld = heldButtons.has(buttonType);
|
|
4257
|
+
if (isHeld) {
|
|
4258
|
+
setHeldButtons((prev) => {
|
|
4259
|
+
const next = new Set(prev);
|
|
4260
|
+
next.delete(buttonType);
|
|
4261
|
+
return next;
|
|
4262
|
+
});
|
|
4263
|
+
onButtonUp(buttonType);
|
|
4264
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
|
|
4265
|
+
} else {
|
|
4266
|
+
setHeldButtons((prev) => {
|
|
4267
|
+
setTurboButtons((turboPrev) => {
|
|
4268
|
+
if (turboPrev.has(buttonType)) {
|
|
4269
|
+
const nextTurbo = new Set(turboPrev);
|
|
4270
|
+
nextTurbo.delete(buttonType);
|
|
4271
|
+
return nextTurbo;
|
|
4272
|
+
}
|
|
4273
|
+
return turboPrev;
|
|
4274
|
+
});
|
|
4275
|
+
return new Set(prev).add(buttonType);
|
|
4276
|
+
});
|
|
4277
|
+
onButtonDown(buttonType);
|
|
4278
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate([10, 30, 10]);
|
|
4279
|
+
}
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
if (isTurboMode) {
|
|
4283
|
+
const isTurbo = turboButtons.has(buttonType);
|
|
4284
|
+
if (isTurbo) {
|
|
4285
|
+
setTurboButtons((prev) => {
|
|
4286
|
+
const next = new Set(prev);
|
|
4287
|
+
next.delete(buttonType);
|
|
4288
|
+
return next;
|
|
4289
|
+
});
|
|
4290
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
|
|
4291
|
+
} else {
|
|
4292
|
+
setTurboButtons((prev) => {
|
|
4293
|
+
setHeldButtons((holdPrev) => {
|
|
4294
|
+
if (holdPrev.has(buttonType)) {
|
|
4295
|
+
const nextHold = new Set(holdPrev);
|
|
4296
|
+
nextHold.delete(buttonType);
|
|
4297
|
+
onButtonUp(buttonType);
|
|
4298
|
+
return nextHold;
|
|
4299
|
+
}
|
|
4300
|
+
return holdPrev;
|
|
4301
|
+
});
|
|
4302
|
+
return new Set(prev).add(buttonType);
|
|
4303
|
+
});
|
|
4304
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate([5, 10, 5]);
|
|
4305
|
+
}
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4088
4308
|
setPressedButtons((prev) => {
|
|
4089
4309
|
if (prev.has(buttonType)) return prev;
|
|
4090
4310
|
const next = new Set(prev);
|
|
4091
4311
|
next.add(buttonType);
|
|
4092
4312
|
return next;
|
|
4093
4313
|
});
|
|
4094
|
-
|
|
4314
|
+
if (!heldButtons.has(buttonType)) {
|
|
4315
|
+
onButtonDown(buttonType);
|
|
4316
|
+
}
|
|
4095
4317
|
},
|
|
4096
|
-
[isRunning, getButtonKeyboardCode]
|
|
4318
|
+
[isRunning, getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonDown, onButtonUp]
|
|
4097
4319
|
);
|
|
4098
4320
|
const handleRelease = React2.useCallback(
|
|
4099
4321
|
(buttonType) => {
|
|
@@ -4101,15 +4323,29 @@ function VirtualController({
|
|
|
4101
4323
|
if (isSystemButton) return;
|
|
4102
4324
|
const keyboardCode = getButtonKeyboardCode(buttonType);
|
|
4103
4325
|
if (!keyboardCode) return;
|
|
4326
|
+
if (isHoldMode) return;
|
|
4327
|
+
if (heldButtons.has(buttonType)) {
|
|
4328
|
+
if (!isHoldMode) {
|
|
4329
|
+
setHeldButtons((prev) => {
|
|
4330
|
+
const next = new Set(prev);
|
|
4331
|
+
next.delete(buttonType);
|
|
4332
|
+
return next;
|
|
4333
|
+
});
|
|
4334
|
+
onButtonUp(buttonType);
|
|
4335
|
+
if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
|
|
4336
|
+
}
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
if (isTurboMode) return;
|
|
4104
4340
|
setPressedButtons((prev) => {
|
|
4105
4341
|
if (!prev.has(buttonType)) return prev;
|
|
4106
4342
|
const next = new Set(prev);
|
|
4107
4343
|
next.delete(buttonType);
|
|
4108
4344
|
return next;
|
|
4109
4345
|
});
|
|
4110
|
-
|
|
4346
|
+
onButtonUp(buttonType);
|
|
4111
4347
|
},
|
|
4112
|
-
[getButtonKeyboardCode]
|
|
4348
|
+
[getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonUp]
|
|
4113
4349
|
);
|
|
4114
4350
|
React2.useEffect(() => {
|
|
4115
4351
|
if (!isRunning && pressedButtons.size > 0) {
|
|
@@ -4121,6 +4357,22 @@ function VirtualController({
|
|
|
4121
4357
|
setPressedButtons(/* @__PURE__ */ new Set());
|
|
4122
4358
|
}
|
|
4123
4359
|
}, [isRunning, pressedButtons, handleRelease]);
|
|
4360
|
+
const TURBO_RATE = 15;
|
|
4361
|
+
React2.useEffect(() => {
|
|
4362
|
+
pressedButtonsRef.current = pressedButtons;
|
|
4363
|
+
}, [pressedButtons]);
|
|
4364
|
+
React2.useEffect(() => {
|
|
4365
|
+
if (isTurboMode || turboButtons.size === 0) return;
|
|
4366
|
+
const interval = setInterval(() => {
|
|
4367
|
+
turboButtons.forEach((buttonType) => {
|
|
4368
|
+
if (pressedButtonsRef.current.has(buttonType)) {
|
|
4369
|
+
onButtonDown(buttonType);
|
|
4370
|
+
setTimeout(() => onButtonUp(buttonType), 25);
|
|
4371
|
+
}
|
|
4372
|
+
});
|
|
4373
|
+
}, 1e3 / TURBO_RATE);
|
|
4374
|
+
return () => clearInterval(interval);
|
|
4375
|
+
}, [isTurboMode, turboButtons, onButtonDown, onButtonUp]);
|
|
4124
4376
|
const memoizedButtonElements = React2.useMemo(() => {
|
|
4125
4377
|
const width = containerSize.width || (typeof window !== "undefined" ? window.innerWidth : 0);
|
|
4126
4378
|
const height = containerSize.height || (typeof window !== "undefined" ? window.innerHeight : 0);
|
|
@@ -4151,14 +4403,32 @@ function VirtualController({
|
|
|
4151
4403
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
4152
4404
|
style: { touchAction: "none" },
|
|
4153
4405
|
children: [
|
|
4154
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4406
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed top-4 left-1/2 -translate-x-1/2 z-40 flex gap-4", children: [
|
|
4407
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4408
|
+
LockButton,
|
|
4409
|
+
{
|
|
4410
|
+
isLocked,
|
|
4411
|
+
onToggle: toggleLock,
|
|
4412
|
+
systemColor
|
|
4413
|
+
}
|
|
4414
|
+
),
|
|
4415
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4416
|
+
HoldButton,
|
|
4417
|
+
{
|
|
4418
|
+
isActive: isHoldMode,
|
|
4419
|
+
onToggle: toggleHoldMode,
|
|
4420
|
+
systemColor
|
|
4421
|
+
}
|
|
4422
|
+
),
|
|
4423
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4424
|
+
TurboButton,
|
|
4425
|
+
{
|
|
4426
|
+
isActive: isTurboMode,
|
|
4427
|
+
onToggle: toggleTurboMode,
|
|
4428
|
+
systemColor
|
|
4429
|
+
}
|
|
4430
|
+
)
|
|
4431
|
+
] }),
|
|
4162
4432
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4163
4433
|
Dpad_default,
|
|
4164
4434
|
{
|
|
@@ -4167,18 +4437,20 @@ function VirtualController({
|
|
|
4167
4437
|
y: finalDpadY,
|
|
4168
4438
|
containerWidth: containerSize.width || window.innerWidth,
|
|
4169
4439
|
containerHeight: containerSize.height || window.innerHeight,
|
|
4170
|
-
controls,
|
|
4171
4440
|
systemColor,
|
|
4172
4441
|
isLandscape,
|
|
4173
4442
|
customPosition: getPosition("up", isLandscape),
|
|
4174
|
-
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
4443
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape),
|
|
4444
|
+
hapticsEnabled,
|
|
4445
|
+
onButtonDown,
|
|
4446
|
+
onButtonUp
|
|
4175
4447
|
}
|
|
4176
4448
|
),
|
|
4177
4449
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4178
4450
|
VirtualButton_default,
|
|
4179
4451
|
{
|
|
4180
4452
|
config: adjustedConfig,
|
|
4181
|
-
isPressed: pressedButtons.has(buttonConfig.type),
|
|
4453
|
+
isPressed: pressedButtons.has(buttonConfig.type) || heldButtons.has(buttonConfig.type),
|
|
4182
4454
|
onPress: handlePress,
|
|
4183
4455
|
onPressDown: handlePressDown,
|
|
4184
4456
|
onRelease: handleRelease,
|
|
@@ -4187,10 +4459,24 @@ function VirtualController({
|
|
|
4187
4459
|
customPosition,
|
|
4188
4460
|
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4189
4461
|
isLandscape,
|
|
4190
|
-
console: layout.console
|
|
4462
|
+
console: layout.console,
|
|
4463
|
+
hapticsEnabled,
|
|
4464
|
+
mode: isHoldMode ? "hold" : isTurboMode ? "turbo" : "normal",
|
|
4465
|
+
isHeld: heldButtons.has(buttonConfig.type),
|
|
4466
|
+
isInTurbo: turboButtons.has(buttonConfig.type)
|
|
4191
4467
|
},
|
|
4192
4468
|
buttonConfig.type
|
|
4193
4469
|
)),
|
|
4470
|
+
(isHoldMode || isTurboMode) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4471
|
+
ModeOverlay,
|
|
4472
|
+
{
|
|
4473
|
+
mode: isHoldMode ? "hold" : "turbo",
|
|
4474
|
+
heldButtons,
|
|
4475
|
+
turboButtons,
|
|
4476
|
+
systemColor,
|
|
4477
|
+
onExit: isHoldMode ? toggleHoldMode : toggleTurboMode
|
|
4478
|
+
}
|
|
4479
|
+
),
|
|
4194
4480
|
/* @__PURE__ */ jsxRuntime.jsx(ControlsHint, { isVisible: isRunning })
|
|
4195
4481
|
]
|
|
4196
4482
|
}
|
|
@@ -4877,7 +5163,8 @@ function CheatModal({
|
|
|
4877
5163
|
cheats,
|
|
4878
5164
|
activeCheats,
|
|
4879
5165
|
onToggle,
|
|
4880
|
-
onClose
|
|
5166
|
+
onClose,
|
|
5167
|
+
onAddManualCheat
|
|
4881
5168
|
}) {
|
|
4882
5169
|
const t = useKoinTranslation();
|
|
4883
5170
|
const [copiedId, setCopiedId] = React2__default.default.useState(null);
|
|
@@ -4895,54 +5182,111 @@ function CheatModal({
|
|
|
4895
5182
|
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4896
5183
|
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
|
|
4897
5184
|
footer: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }),
|
|
4898
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4899
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4918
|
-
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4919
|
-
`,
|
|
4920
|
-
children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
|
|
5185
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-4 max-h-[400px] overflow-y-auto", children: [
|
|
5186
|
+
onAddManualCheat && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-white/5 rounded-lg border border-white/10 space-y-3", children: [
|
|
5187
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5188
|
+
"input",
|
|
5189
|
+
{
|
|
5190
|
+
type: "text",
|
|
5191
|
+
placeholder: t.modals.cheats.codePlaceholder || "Enter cheat code",
|
|
5192
|
+
className: "flex-1 bg-black/50 border border-white/20 rounded px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500",
|
|
5193
|
+
onKeyDown: (e) => {
|
|
5194
|
+
if (e.key === "Enter") {
|
|
5195
|
+
const input = e.currentTarget;
|
|
5196
|
+
const code = input.value.trim();
|
|
5197
|
+
if (code) {
|
|
5198
|
+
const descInput = input.parentElement?.nextElementSibling?.querySelector("input");
|
|
5199
|
+
const desc = descInput?.value.trim() || "Custom Cheat";
|
|
5200
|
+
onAddManualCheat(code, desc);
|
|
5201
|
+
input.value = "";
|
|
5202
|
+
if (descInput) descInput.value = "";
|
|
5203
|
+
}
|
|
4921
5204
|
}
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
) }),
|
|
5208
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
5209
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5210
|
+
"input",
|
|
5211
|
+
{
|
|
5212
|
+
type: "text",
|
|
5213
|
+
placeholder: t.modals.cheats.descPlaceholder || "Description (optional)",
|
|
5214
|
+
className: "flex-1 bg-black/50 border border-white/20 rounded px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
|
|
5215
|
+
}
|
|
5216
|
+
),
|
|
5217
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5218
|
+
"button",
|
|
5219
|
+
{
|
|
5220
|
+
onClick: (e) => {
|
|
5221
|
+
const descInput = e.currentTarget.previousElementSibling;
|
|
5222
|
+
const codeInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input");
|
|
5223
|
+
const code = codeInput?.value.trim();
|
|
5224
|
+
const desc = descInput?.value.trim() || "Custom Cheat";
|
|
5225
|
+
if (code) {
|
|
5226
|
+
onAddManualCheat(code, desc);
|
|
5227
|
+
codeInput.value = "";
|
|
5228
|
+
descInput.value = "";
|
|
5229
|
+
}
|
|
5230
|
+
},
|
|
5231
|
+
className: "px-3 py-2 bg-purple-600 hover:bg-purple-500 text-white text-sm rounded transition-colors",
|
|
5232
|
+
children: t.modals.cheats.add || "Add"
|
|
5233
|
+
}
|
|
5234
|
+
)
|
|
5235
|
+
] })
|
|
5236
|
+
] }),
|
|
5237
|
+
cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
5238
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
5239
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
5240
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
5241
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: cheats.map((cheat) => {
|
|
5242
|
+
const isActive = activeCheats.has(cheat.id);
|
|
5243
|
+
const isManual = cheat.source === "manual";
|
|
5244
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5245
|
+
"div",
|
|
5246
|
+
{
|
|
5247
|
+
className: `
|
|
5248
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
5249
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
5250
|
+
`,
|
|
5251
|
+
onClick: () => onToggle(cheat.id),
|
|
5252
|
+
children: [
|
|
5253
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5254
|
+
"div",
|
|
5255
|
+
{
|
|
5256
|
+
className: `
|
|
5257
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
5258
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
5259
|
+
`,
|
|
5260
|
+
children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
|
|
5261
|
+
}
|
|
5262
|
+
),
|
|
5263
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
5264
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5265
|
+
isManual && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs bg-purple-500/20 text-purple-300 px-1.5 py-0.5 rounded border border-purple-500/30", children: "USER" }),
|
|
5266
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description })
|
|
5267
|
+
] }),
|
|
5268
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
5269
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "px-2 py-1 bg-black/50 rounded text-xs font-mono text-gray-400 truncate max-w-[200px]", children: cheat.code }),
|
|
5270
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5271
|
+
"button",
|
|
5272
|
+
{
|
|
5273
|
+
onClick: (e) => {
|
|
5274
|
+
e.stopPropagation();
|
|
5275
|
+
handleCopy(cheat.code, cheat.id);
|
|
5276
|
+
},
|
|
5277
|
+
className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
|
|
5278
|
+
title: t.modals.cheats.copy,
|
|
5279
|
+
children: copiedId === cheat.id ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { size: 14 })
|
|
5280
|
+
}
|
|
5281
|
+
)
|
|
5282
|
+
] })
|
|
4939
5283
|
] })
|
|
4940
|
-
]
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
)
|
|
4945
|
-
|
|
5284
|
+
]
|
|
5285
|
+
},
|
|
5286
|
+
cheat.id
|
|
5287
|
+
);
|
|
5288
|
+
}) })
|
|
5289
|
+
] })
|
|
4946
5290
|
}
|
|
4947
5291
|
);
|
|
4948
5292
|
}
|
|
@@ -5274,7 +5618,9 @@ function SettingsModal({
|
|
|
5274
5618
|
onClose,
|
|
5275
5619
|
currentLanguage,
|
|
5276
5620
|
onLanguageChange,
|
|
5277
|
-
systemColor = "#00FF41"
|
|
5621
|
+
systemColor = "#00FF41",
|
|
5622
|
+
hapticsEnabled,
|
|
5623
|
+
onToggleHaptics
|
|
5278
5624
|
}) {
|
|
5279
5625
|
const t = useKoinTranslation();
|
|
5280
5626
|
const languages = [
|
|
@@ -5298,30 +5644,57 @@ function SettingsModal({
|
|
|
5298
5644
|
children: t.modals.shortcuts.pressEsc
|
|
5299
5645
|
}
|
|
5300
5646
|
),
|
|
5301
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
5302
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
5303
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5304
|
-
|
|
5647
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 space-y-6", children: [
|
|
5648
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5649
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5650
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
|
|
5651
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
|
|
5652
|
+
] }),
|
|
5653
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
|
|
5654
|
+
const isActive = currentLanguage === lang.code;
|
|
5655
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5656
|
+
"button",
|
|
5657
|
+
{
|
|
5658
|
+
onClick: () => onLanguageChange(lang.code),
|
|
5659
|
+
className: `
|
|
5660
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5661
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5662
|
+
`,
|
|
5663
|
+
children: [
|
|
5664
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
|
|
5665
|
+
isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
|
|
5666
|
+
]
|
|
5667
|
+
},
|
|
5668
|
+
lang.code
|
|
5669
|
+
);
|
|
5670
|
+
}) })
|
|
5305
5671
|
] }),
|
|
5306
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5307
|
-
|
|
5308
|
-
|
|
5672
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5673
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5674
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 16 }),
|
|
5675
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.haptics })
|
|
5676
|
+
] }),
|
|
5677
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
5309
5678
|
"button",
|
|
5310
5679
|
{
|
|
5311
|
-
onClick:
|
|
5680
|
+
onClick: onToggleHaptics,
|
|
5312
5681
|
className: `
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5682
|
+
w-full flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5683
|
+
${hapticsEnabled ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5684
|
+
`,
|
|
5316
5685
|
children: [
|
|
5317
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children:
|
|
5318
|
-
|
|
5686
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.enableHaptics }),
|
|
5687
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-10 h-6 rounded-full p-1 transition-colors ${hapticsEnabled ? "bg-[#00FF41]" : "bg-gray-700"}`, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5688
|
+
"div",
|
|
5689
|
+
{
|
|
5690
|
+
className: `w-4 h-4 rounded-full bg-white transition-transform ${hapticsEnabled ? "translate-x-4" : "translate-x-0"}`
|
|
5691
|
+
}
|
|
5692
|
+
) })
|
|
5319
5693
|
]
|
|
5320
|
-
}
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
] }) })
|
|
5694
|
+
}
|
|
5695
|
+
)
|
|
5696
|
+
] })
|
|
5697
|
+
] })
|
|
5325
5698
|
}
|
|
5326
5699
|
);
|
|
5327
5700
|
}
|
|
@@ -5341,6 +5714,7 @@ function GameModals({
|
|
|
5341
5714
|
cheats,
|
|
5342
5715
|
activeCheats,
|
|
5343
5716
|
onToggleCheat,
|
|
5717
|
+
onAddManualCheat,
|
|
5344
5718
|
saveModalOpen,
|
|
5345
5719
|
setSaveModalOpen,
|
|
5346
5720
|
saveModalMode,
|
|
@@ -5360,7 +5734,9 @@ function GameModals({
|
|
|
5360
5734
|
settingsModalOpen,
|
|
5361
5735
|
setSettingsModalOpen,
|
|
5362
5736
|
currentLanguage,
|
|
5363
|
-
onLanguageChange
|
|
5737
|
+
onLanguageChange,
|
|
5738
|
+
hapticsEnabled,
|
|
5739
|
+
onToggleHaptics
|
|
5364
5740
|
}) {
|
|
5365
5741
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
5366
5742
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -5395,6 +5771,7 @@ function GameModals({
|
|
|
5395
5771
|
cheats,
|
|
5396
5772
|
activeCheats,
|
|
5397
5773
|
onToggle: onToggleCheat,
|
|
5774
|
+
onAddManualCheat,
|
|
5398
5775
|
onClose: () => {
|
|
5399
5776
|
setCheatsModalOpen(false);
|
|
5400
5777
|
onResume();
|
|
@@ -5447,7 +5824,9 @@ function GameModals({
|
|
|
5447
5824
|
},
|
|
5448
5825
|
currentLanguage,
|
|
5449
5826
|
onLanguageChange,
|
|
5450
|
-
systemColor
|
|
5827
|
+
systemColor,
|
|
5828
|
+
hapticsEnabled,
|
|
5829
|
+
onToggleHaptics
|
|
5451
5830
|
}
|
|
5452
5831
|
)
|
|
5453
5832
|
] });
|
|
@@ -6742,6 +7121,11 @@ function useEmulatorCore({
|
|
|
6742
7121
|
input_volume_up: "add",
|
|
6743
7122
|
input_volume_down: "subtract",
|
|
6744
7123
|
input_audio_mute: "f9",
|
|
7124
|
+
// Cheat hotkeys
|
|
7125
|
+
quick_menu_show_cheats: true,
|
|
7126
|
+
input_cheat_index_plus: "y",
|
|
7127
|
+
input_cheat_index_minus: "t",
|
|
7128
|
+
input_cheat_toggle: "u",
|
|
6745
7129
|
...inputConfig,
|
|
6746
7130
|
...specificConfig,
|
|
6747
7131
|
// Apply system-specific optimizations
|
|
@@ -6835,18 +7219,15 @@ function useEmulatorCore({
|
|
|
6835
7219
|
const restart = React2.useCallback(async () => {
|
|
6836
7220
|
if (nostalgistRef.current) {
|
|
6837
7221
|
try {
|
|
6838
|
-
|
|
6839
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
6840
|
-
nostalgistRef.current.resume();
|
|
6841
|
-
setIsPaused(false);
|
|
6842
|
-
setStatus("running");
|
|
6843
|
-
} catch (err) {
|
|
6844
|
-
console.error("[Nostalgist] Restart error:", err);
|
|
7222
|
+
console.log("[Nostalgist] Full restart - stopping, re-preparing with fresh config, starting");
|
|
6845
7223
|
stop();
|
|
7224
|
+
await prepare();
|
|
6846
7225
|
await start();
|
|
7226
|
+
} catch (err) {
|
|
7227
|
+
console.error("[Nostalgist] Restart error:", err);
|
|
6847
7228
|
}
|
|
6848
7229
|
}
|
|
6849
|
-
}, [stop, start]);
|
|
7230
|
+
}, [stop, prepare, start]);
|
|
6850
7231
|
const pause = React2.useCallback(() => {
|
|
6851
7232
|
if (nostalgistRef.current && !isPaused && status === "running") {
|
|
6852
7233
|
try {
|
|
@@ -6939,9 +7320,6 @@ function useEmulatorCore({
|
|
|
6939
7320
|
console.error("[Nostalgist] Resize error:", err);
|
|
6940
7321
|
}
|
|
6941
7322
|
}, []);
|
|
6942
|
-
React2.useCallback(() => {
|
|
6943
|
-
return nostalgistRef.current;
|
|
6944
|
-
}, []);
|
|
6945
7323
|
return {
|
|
6946
7324
|
status,
|
|
6947
7325
|
setStatus,
|
|
@@ -7068,8 +7446,26 @@ function useEmulatorInput({ nostalgistRef }) {
|
|
|
7068
7446
|
console.error("[Nostalgist] Press key error:", err);
|
|
7069
7447
|
}
|
|
7070
7448
|
}, [nostalgistRef]);
|
|
7449
|
+
const pressDown = React2.useCallback((button) => {
|
|
7450
|
+
if (!nostalgistRef.current) return;
|
|
7451
|
+
try {
|
|
7452
|
+
nostalgistRef.current.pressDown(button);
|
|
7453
|
+
} catch (err) {
|
|
7454
|
+
console.error("[Nostalgist] Press down error:", err);
|
|
7455
|
+
}
|
|
7456
|
+
}, [nostalgistRef]);
|
|
7457
|
+
const pressUp = React2.useCallback((button) => {
|
|
7458
|
+
if (!nostalgistRef.current) return;
|
|
7459
|
+
try {
|
|
7460
|
+
nostalgistRef.current.pressUp(button);
|
|
7461
|
+
} catch (err) {
|
|
7462
|
+
console.error("[Nostalgist] Press up error:", err);
|
|
7463
|
+
}
|
|
7464
|
+
}, [nostalgistRef]);
|
|
7071
7465
|
return {
|
|
7072
|
-
pressKey
|
|
7466
|
+
pressKey,
|
|
7467
|
+
pressDown,
|
|
7468
|
+
pressUp
|
|
7073
7469
|
};
|
|
7074
7470
|
}
|
|
7075
7471
|
var MIN_SAVE_INTERVAL = 100;
|
|
@@ -7336,26 +7732,53 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
|
|
|
7336
7732
|
stopRewindCapture
|
|
7337
7733
|
};
|
|
7338
7734
|
}
|
|
7339
|
-
function useEmulatorCheats({
|
|
7340
|
-
|
|
7735
|
+
function useEmulatorCheats({
|
|
7736
|
+
nostalgistRef
|
|
7737
|
+
}) {
|
|
7738
|
+
const allocatedPointersRef = React2.useRef([]);
|
|
7739
|
+
const injectCheats = React2.useCallback((cheats) => {
|
|
7341
7740
|
if (!nostalgistRef.current) return;
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7741
|
+
const module = nostalgistRef.current.getEmscriptenModule();
|
|
7742
|
+
if (!module) return;
|
|
7743
|
+
if (module._free && allocatedPointersRef.current.length > 0) {
|
|
7744
|
+
allocatedPointersRef.current.forEach((ptr) => {
|
|
7745
|
+
try {
|
|
7746
|
+
module._free(ptr);
|
|
7747
|
+
} catch (e) {
|
|
7748
|
+
}
|
|
7749
|
+
});
|
|
7750
|
+
allocatedPointersRef.current = [];
|
|
7346
7751
|
}
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
}
|
|
7353
|
-
|
|
7752
|
+
if (module._cmd_cheat_realloc) {
|
|
7753
|
+
module._cmd_cheat_realloc(0);
|
|
7754
|
+
if (cheats.length > 0) {
|
|
7755
|
+
module._cmd_cheat_realloc(cheats.length);
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
if (cheats.length > 0 && module.stringToNewUTF8 && module._cmd_cheat_set_code) {
|
|
7759
|
+
cheats.forEach((cheat, index) => {
|
|
7760
|
+
try {
|
|
7761
|
+
const ptr = module.stringToNewUTF8(cheat.code);
|
|
7762
|
+
allocatedPointersRef.current.push(ptr);
|
|
7763
|
+
module._cmd_cheat_set_code(index, ptr);
|
|
7764
|
+
if (module._cmd_cheat_toggle_index) {
|
|
7765
|
+
module._cmd_cheat_toggle_index(index, true);
|
|
7766
|
+
}
|
|
7767
|
+
} catch (err) {
|
|
7768
|
+
console.error("[Cheats] Failed to inject cheat:", index, err);
|
|
7769
|
+
}
|
|
7770
|
+
});
|
|
7771
|
+
}
|
|
7772
|
+
if (module._cmd_cheat_apply_cheats) {
|
|
7773
|
+
module._cmd_cheat_apply_cheats();
|
|
7354
7774
|
}
|
|
7355
7775
|
}, [nostalgistRef]);
|
|
7776
|
+
const clearCheats = React2.useCallback(() => {
|
|
7777
|
+
injectCheats([]);
|
|
7778
|
+
}, [injectCheats]);
|
|
7356
7779
|
return {
|
|
7357
|
-
|
|
7358
|
-
|
|
7780
|
+
injectCheats,
|
|
7781
|
+
clearCheats
|
|
7359
7782
|
};
|
|
7360
7783
|
}
|
|
7361
7784
|
|
|
@@ -7427,7 +7850,9 @@ var useNostalgist = ({
|
|
|
7427
7850
|
initialVolume
|
|
7428
7851
|
});
|
|
7429
7852
|
const {
|
|
7430
|
-
pressKey
|
|
7853
|
+
pressKey,
|
|
7854
|
+
pressDown,
|
|
7855
|
+
pressUp
|
|
7431
7856
|
} = useEmulatorInput({
|
|
7432
7857
|
nostalgistRef
|
|
7433
7858
|
});
|
|
@@ -7450,8 +7875,8 @@ var useNostalgist = ({
|
|
|
7450
7875
|
// Disable manual rewind loop for heavy systems
|
|
7451
7876
|
});
|
|
7452
7877
|
const {
|
|
7453
|
-
|
|
7454
|
-
|
|
7878
|
+
injectCheats,
|
|
7879
|
+
clearCheats
|
|
7455
7880
|
} = useEmulatorCheats({
|
|
7456
7881
|
nostalgistRef
|
|
7457
7882
|
});
|
|
@@ -7494,9 +7919,11 @@ var useNostalgist = ({
|
|
|
7494
7919
|
toggleMute,
|
|
7495
7920
|
screenshot,
|
|
7496
7921
|
pressKey,
|
|
7922
|
+
pressDown,
|
|
7923
|
+
pressUp,
|
|
7497
7924
|
resize,
|
|
7498
|
-
|
|
7499
|
-
|
|
7925
|
+
injectCheats,
|
|
7926
|
+
clearCheats,
|
|
7500
7927
|
getNostalgistInstance,
|
|
7501
7928
|
isPerformanceMode
|
|
7502
7929
|
}), [
|
|
@@ -7526,9 +7953,11 @@ var useNostalgist = ({
|
|
|
7526
7953
|
toggleMute,
|
|
7527
7954
|
screenshot,
|
|
7528
7955
|
pressKey,
|
|
7956
|
+
pressDown,
|
|
7957
|
+
pressUp,
|
|
7529
7958
|
resize,
|
|
7530
|
-
|
|
7531
|
-
|
|
7959
|
+
injectCheats,
|
|
7960
|
+
clearCheats,
|
|
7532
7961
|
getNostalgistInstance,
|
|
7533
7962
|
isPerformanceMode
|
|
7534
7963
|
]);
|
|
@@ -8259,39 +8688,95 @@ function useGameSaves({
|
|
|
8259
8688
|
handleAutoSaveToggle
|
|
8260
8689
|
};
|
|
8261
8690
|
}
|
|
8691
|
+
var generateManualId = () => `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8262
8692
|
function useGameCheats({
|
|
8263
8693
|
nostalgist,
|
|
8264
8694
|
cheats = [],
|
|
8265
|
-
onToggleCheat
|
|
8695
|
+
onToggleCheat,
|
|
8696
|
+
showToast,
|
|
8697
|
+
romId
|
|
8266
8698
|
}) {
|
|
8267
8699
|
const [cheatsModalOpen, setCheatsModalOpen] = React2.useState(false);
|
|
8268
8700
|
const [activeCheats, setActiveCheats] = React2.useState(/* @__PURE__ */ new Set());
|
|
8701
|
+
const [manualCheatsInternal, setManualCheatsInternal] = React2.useState([]);
|
|
8702
|
+
const [isLoaded, setIsLoaded] = React2.useState(false);
|
|
8703
|
+
const cheatStorageKey = romId ? `koin_cheats_${romId}` : null;
|
|
8704
|
+
React2.useEffect(() => {
|
|
8705
|
+
if (!cheatStorageKey) return;
|
|
8706
|
+
setIsLoaded(false);
|
|
8707
|
+
try {
|
|
8708
|
+
const stored = localStorage.getItem(cheatStorageKey);
|
|
8709
|
+
if (stored) {
|
|
8710
|
+
const parsed = JSON.parse(stored);
|
|
8711
|
+
if (Array.isArray(parsed)) {
|
|
8712
|
+
setManualCheatsInternal(parsed);
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
} catch (e) {
|
|
8716
|
+
console.error("[Cheats] Failed to load cheats:", e);
|
|
8717
|
+
} finally {
|
|
8718
|
+
setIsLoaded(true);
|
|
8719
|
+
}
|
|
8720
|
+
}, [cheatStorageKey]);
|
|
8721
|
+
React2.useEffect(() => {
|
|
8722
|
+
if (!cheatStorageKey || !isLoaded) return;
|
|
8723
|
+
localStorage.setItem(cheatStorageKey, JSON.stringify(manualCheatsInternal));
|
|
8724
|
+
}, [manualCheatsInternal, cheatStorageKey, isLoaded]);
|
|
8725
|
+
const allCheats = React2.useMemo(() => {
|
|
8726
|
+
const normalizedExternal = cheats.map((c) => ({
|
|
8727
|
+
id: typeof c.id === "number" ? `db-${c.id}` : c.id,
|
|
8728
|
+
code: c.code,
|
|
8729
|
+
description: c.description,
|
|
8730
|
+
source: "database"
|
|
8731
|
+
}));
|
|
8732
|
+
return [...normalizedExternal, ...manualCheatsInternal];
|
|
8733
|
+
}, [cheats, manualCheatsInternal]);
|
|
8734
|
+
const handleAddManualCheat = (code, description) => {
|
|
8735
|
+
if (!nostalgist) return;
|
|
8736
|
+
const newCheat = {
|
|
8737
|
+
id: generateManualId(),
|
|
8738
|
+
code,
|
|
8739
|
+
description,
|
|
8740
|
+
source: "manual"
|
|
8741
|
+
};
|
|
8742
|
+
setManualCheatsInternal((prev) => [...prev, newCheat]);
|
|
8743
|
+
setActiveCheats((prev) => new Set(prev).add(newCheat.id));
|
|
8744
|
+
setCheatsModalOpen(false);
|
|
8745
|
+
const currentActiveCheats = [...activeCheats, newCheat.id];
|
|
8746
|
+
const cheatsToInject = allCheats.filter((c) => currentActiveCheats.includes(c.id) || c.id === newCheat.id).concat([newCheat]);
|
|
8747
|
+
nostalgist.injectCheats(cheatsToInject.map((c) => ({ code: c.code })));
|
|
8748
|
+
nostalgist.resume();
|
|
8749
|
+
showToast?.("Cheat added!", "success");
|
|
8750
|
+
};
|
|
8269
8751
|
const handleToggleCheat = (cheatId) => {
|
|
8270
8752
|
if (!nostalgist) return;
|
|
8271
8753
|
const newActiveCheats = new Set(activeCheats);
|
|
8272
8754
|
const isActive = newActiveCheats.has(cheatId);
|
|
8273
8755
|
if (isActive) {
|
|
8274
8756
|
newActiveCheats.delete(cheatId);
|
|
8757
|
+
showToast?.("Cheat Disabled");
|
|
8275
8758
|
} else {
|
|
8276
8759
|
newActiveCheats.add(cheatId);
|
|
8760
|
+
showToast?.("Cheat Enabled", "success");
|
|
8277
8761
|
}
|
|
8278
8762
|
setActiveCheats(newActiveCheats);
|
|
8279
|
-
onToggleCheat
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
});
|
|
8288
|
-
}, 50);
|
|
8763
|
+
if (onToggleCheat) {
|
|
8764
|
+
const numericId = cheatId.startsWith("db-") ? parseInt(cheatId.slice(3), 10) : void 0;
|
|
8765
|
+
if (numericId !== void 0) {
|
|
8766
|
+
onToggleCheat(numericId, !isActive);
|
|
8767
|
+
}
|
|
8768
|
+
}
|
|
8769
|
+
const cheatsToInject = allCheats.filter((c) => newActiveCheats.has(c.id)).map((c) => ({ code: c.code }));
|
|
8770
|
+
nostalgist.injectCheats(cheatsToInject);
|
|
8289
8771
|
};
|
|
8290
8772
|
return {
|
|
8291
8773
|
cheatsModalOpen,
|
|
8292
8774
|
setCheatsModalOpen,
|
|
8293
8775
|
activeCheats,
|
|
8294
|
-
|
|
8776
|
+
allCheats,
|
|
8777
|
+
// Unified list - replaces separate cheats + manualCheats
|
|
8778
|
+
handleToggleCheat,
|
|
8779
|
+
handleAddManualCheat
|
|
8295
8780
|
};
|
|
8296
8781
|
}
|
|
8297
8782
|
function useGameRecording({
|
|
@@ -8468,10 +8953,13 @@ function useGamePlayer(props) {
|
|
|
8468
8953
|
cheatsModalOpen,
|
|
8469
8954
|
setCheatsModalOpen,
|
|
8470
8955
|
activeCheats,
|
|
8471
|
-
|
|
8956
|
+
allCheats,
|
|
8957
|
+
handleToggleCheat,
|
|
8958
|
+
handleAddManualCheat
|
|
8472
8959
|
} = useGameCheats({
|
|
8473
8960
|
...props,
|
|
8474
|
-
nostalgist
|
|
8961
|
+
nostalgist,
|
|
8962
|
+
showToast
|
|
8475
8963
|
});
|
|
8476
8964
|
const {
|
|
8477
8965
|
isRecording,
|
|
@@ -8527,7 +9015,9 @@ function useGamePlayer(props) {
|
|
|
8527
9015
|
hardcoreRestrictions,
|
|
8528
9016
|
// Cheats
|
|
8529
9017
|
activeCheats,
|
|
9018
|
+
allCheats,
|
|
8530
9019
|
handleToggleCheat,
|
|
9020
|
+
handleAddManualCheat,
|
|
8531
9021
|
// Emulator
|
|
8532
9022
|
nostalgist,
|
|
8533
9023
|
volumeState,
|
|
@@ -8555,7 +9045,8 @@ var DEFAULT_SETTINGS = {
|
|
|
8555
9045
|
muted: false,
|
|
8556
9046
|
shader: "",
|
|
8557
9047
|
showPerformanceOverlay: false,
|
|
8558
|
-
showInputDisplay: false
|
|
9048
|
+
showInputDisplay: false,
|
|
9049
|
+
hapticsEnabled: true
|
|
8559
9050
|
};
|
|
8560
9051
|
function usePlayerPersistence(onSettingsChange) {
|
|
8561
9052
|
const [settings, setSettings] = React2.useState(DEFAULT_SETTINGS);
|
|
@@ -8662,7 +9153,9 @@ var es = {
|
|
|
8662
9153
|
shortcuts: "Atajos",
|
|
8663
9154
|
exit: "Salir",
|
|
8664
9155
|
language: "Idioma",
|
|
8665
|
-
selectLanguage: "Seleccionar idioma"
|
|
9156
|
+
selectLanguage: "Seleccionar idioma",
|
|
9157
|
+
haptics: "Vibraci\xF3n",
|
|
9158
|
+
enableHaptics: "Habilitar vibraci\xF3n"
|
|
8666
9159
|
},
|
|
8667
9160
|
overlay: {
|
|
8668
9161
|
play: "JUGAR",
|
|
@@ -8745,7 +9238,10 @@ var es = {
|
|
|
8745
9238
|
emptyDesc: "No se encontraron c\xF3digos para este juego",
|
|
8746
9239
|
copy: "Copiar c\xF3digo",
|
|
8747
9240
|
active: "{{count}} truco{{s}} activo(s)",
|
|
8748
|
-
toggleHint: "Haz clic para activar/desactivar"
|
|
9241
|
+
toggleHint: "Haz clic para activar/desactivar",
|
|
9242
|
+
codePlaceholder: "Introduce c\xF3digo (ej: 00C-048-E6E)",
|
|
9243
|
+
descPlaceholder: "Descripci\xF3n (opcional)",
|
|
9244
|
+
add: "A\xF1adir truco"
|
|
8749
9245
|
},
|
|
8750
9246
|
saveSlots: {
|
|
8751
9247
|
title: "Guardar partida",
|
|
@@ -8890,7 +9386,9 @@ var fr = {
|
|
|
8890
9386
|
shortcuts: "Raccourcis",
|
|
8891
9387
|
exit: "Quitter",
|
|
8892
9388
|
language: "Langue",
|
|
8893
|
-
selectLanguage: "Choisir la langue"
|
|
9389
|
+
selectLanguage: "Choisir la langue",
|
|
9390
|
+
haptics: "Vibration",
|
|
9391
|
+
enableHaptics: "Activer la vibration"
|
|
8894
9392
|
},
|
|
8895
9393
|
overlay: {
|
|
8896
9394
|
play: "JOUER",
|
|
@@ -8973,7 +9471,10 @@ var fr = {
|
|
|
8973
9471
|
emptyDesc: "Aucun code trouv\xE9 pour ce jeu",
|
|
8974
9472
|
copy: "Copier",
|
|
8975
9473
|
active: "{{count}} actif(s)",
|
|
8976
|
-
toggleHint: "Cliquez pour activer/d\xE9sactiver"
|
|
9474
|
+
toggleHint: "Cliquez pour activer/d\xE9sactiver",
|
|
9475
|
+
codePlaceholder: "Entrez le code (ex: 00C-048-E6E)",
|
|
9476
|
+
descPlaceholder: "Description (optionnel)",
|
|
9477
|
+
add: "Ajouter"
|
|
8977
9478
|
},
|
|
8978
9479
|
saveSlots: {
|
|
8979
9480
|
title: "Sauvegardes",
|
|
@@ -9146,7 +9647,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9146
9647
|
hardcoreRestrictions,
|
|
9147
9648
|
// Cheats
|
|
9148
9649
|
activeCheats,
|
|
9650
|
+
allCheats,
|
|
9149
9651
|
handleToggleCheat,
|
|
9652
|
+
handleAddManualCheat,
|
|
9150
9653
|
// Emulator Instance & State
|
|
9151
9654
|
nostalgist,
|
|
9152
9655
|
// Contains start, restart, etc.
|
|
@@ -9211,7 +9714,7 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9211
9714
|
if (muted !== settings.muted) toggleMute();
|
|
9212
9715
|
}
|
|
9213
9716
|
}, [settingsLoaded]);
|
|
9214
|
-
const { system, systemColor = "#00FF41",
|
|
9717
|
+
const { system, systemColor = "#00FF41", onExit } = props;
|
|
9215
9718
|
const handlePauseToggle = React2.useCallback(() => {
|
|
9216
9719
|
status === "ready" ? start() : togglePause();
|
|
9217
9720
|
}, [status, start, togglePause]);
|
|
@@ -9267,6 +9770,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9267
9770
|
const handleToggleInputDisplay = React2.useCallback(() => {
|
|
9268
9771
|
updateSettings({ showInputDisplay: !settings.showInputDisplay });
|
|
9269
9772
|
}, [updateSettings, settings.showInputDisplay]);
|
|
9773
|
+
const handleToggleHaptics = React2.useCallback(() => {
|
|
9774
|
+
updateSettings({ hapticsEnabled: !settings.hapticsEnabled });
|
|
9775
|
+
}, [updateSettings, settings.hapticsEnabled]);
|
|
9270
9776
|
const handleToggleRecording = React2.useCallback(async () => {
|
|
9271
9777
|
if (!recordingSupported) {
|
|
9272
9778
|
console.warn("[Recording] Not supported in this browser");
|
|
@@ -9353,7 +9859,12 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9353
9859
|
system,
|
|
9354
9860
|
isRunning: status === "running" || status === "paused",
|
|
9355
9861
|
controls,
|
|
9356
|
-
systemColor
|
|
9862
|
+
systemColor,
|
|
9863
|
+
hapticsEnabled: settings.hapticsEnabled,
|
|
9864
|
+
onButtonDown: nostalgist.pressDown,
|
|
9865
|
+
onButtonUp: nostalgist.pressUp,
|
|
9866
|
+
onPause: nostalgist.pause,
|
|
9867
|
+
onResume: nostalgist.resume
|
|
9357
9868
|
}
|
|
9358
9869
|
),
|
|
9359
9870
|
!isFullscreen2 && isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -9508,9 +10019,10 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9508
10019
|
systemColor,
|
|
9509
10020
|
cheatsModalOpen,
|
|
9510
10021
|
setCheatsModalOpen,
|
|
9511
|
-
cheats,
|
|
10022
|
+
cheats: allCheats,
|
|
9512
10023
|
activeCheats,
|
|
9513
10024
|
onToggleCheat: handleToggleCheat,
|
|
10025
|
+
onAddManualCheat: handleAddManualCheat,
|
|
9514
10026
|
saveModalOpen,
|
|
9515
10027
|
setSaveModalOpen,
|
|
9516
10028
|
saveModalMode,
|
|
@@ -9530,7 +10042,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9530
10042
|
settingsModalOpen,
|
|
9531
10043
|
setSettingsModalOpen,
|
|
9532
10044
|
currentLanguage: props.currentLanguage,
|
|
9533
|
-
onLanguageChange: props.onLanguageChange
|
|
10045
|
+
onLanguageChange: props.onLanguageChange,
|
|
10046
|
+
hapticsEnabled: settings.hapticsEnabled,
|
|
10047
|
+
onToggleHaptics: handleToggleHaptics
|
|
9534
10048
|
}
|
|
9535
10049
|
),
|
|
9536
10050
|
!isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|