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.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: "{{count}} cheat{{s}} available",
418
- emptyTitle: "No cheats available",
419
- emptyDesc: "No cheat codes found for this game",
420
- copy: "Copy code",
421
- active: "{{count}} cheat{{s}} active",
422
- toggleHint: "Click a cheat to toggle it on/off"
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: 20,
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.jsx(
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: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "drop-shadow-md", children: label })
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/lib/controls/types.ts
2960
- var DPAD_BUTTONS2 = ["up", "down", "left", "right"];
2961
- var FACE_BUTTONS = ["a", "b", "x", "y"];
2962
- var SHOULDER_BUTTONS = ["l", "r"];
2963
- var TRIGGER_BUTTONS = ["l2", "r2"];
2964
- var STICK_BUTTONS = ["l3", "r3"];
2965
- var SYSTEM_BUTTONS = ["start", "select"];
2966
- var ALL_BUTTONS = [
2967
- ...DPAD_BUTTONS2,
2968
- ...FACE_BUTTONS,
2969
- ...SHOULDER_BUTTONS,
2970
- ...TRIGGER_BUTTONS,
2971
- ...STICK_BUTTONS,
2972
- ...SYSTEM_BUTTONS
2973
- ];
2974
-
2975
- // src/lib/controls/defaults.ts
2976
- var STANDARD_GAMEPAD_BUTTONS = {
2977
- // Face buttons (Xbox/PlayStation layout)
2978
- a: 0,
2979
- // A / Cross (bottom)
2980
- b: 1,
2981
- // B / Circle (right)
2982
- x: 2,
2983
- // X / Square (left)
2984
- y: 3,
2985
- // Y / Triangle (top)
2986
- // Shoulders
2987
- l: 4,
2988
- // LB / L1
2989
- r: 5,
2990
- // RB / R1
2991
- // Triggers
2992
- l2: 6,
2993
- // LT / L2
2994
- r2: 7,
2995
- // RT / R2
2996
- // System
2997
- select: 8,
2998
- // Back / Select / Share
2999
- start: 9,
3000
- // Start / Options
3001
- // Stick clicks
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-4", children: [
3919
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "Long-press" }),
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: "fixed top-4 left-1/2 -translate-x-1/2 z-40 pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
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
- dispatchKeyboardEvent("keydown", keyboardCode);
4236
+ onButtonDown(buttonType);
4070
4237
  setTimeout(() => {
4071
- dispatchKeyboardEvent("keyup", keyboardCode);
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
- dispatchKeyboardEvent("keydown", keyboardCode);
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
- dispatchKeyboardEvent("keyup", keyboardCode);
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.jsx(
4155
- LockButton,
4156
- {
4157
- isLocked,
4158
- onToggle: toggleLock,
4159
- systemColor
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.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4899
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4900
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4901
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
4902
- ] }) : cheats.map((cheat) => {
4903
- const isActive = activeCheats.has(cheat.id);
4904
- return /* @__PURE__ */ jsxRuntime.jsxs(
4905
- "div",
4906
- {
4907
- className: `
4908
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4909
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4910
- `,
4911
- onClick: () => onToggle(cheat.id),
4912
- children: [
4913
- /* @__PURE__ */ jsxRuntime.jsx(
4914
- "div",
4915
- {
4916
- className: `
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
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4924
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description }),
4925
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
4926
- /* @__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 }),
4927
- /* @__PURE__ */ jsxRuntime.jsx(
4928
- "button",
4929
- {
4930
- onClick: (e) => {
4931
- e.stopPropagation();
4932
- handleCopy(cheat.code, cheat.id);
4933
- },
4934
- className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
4935
- title: t.modals.cheats.copy,
4936
- children: copiedId === cheat.id ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { size: 14 })
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
- cheat.id
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.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
5302
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5303
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
5304
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
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.jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
5307
- const isActive = currentLanguage === lang.code;
5308
- return /* @__PURE__ */ jsxRuntime.jsxs(
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: () => onLanguageChange(lang.code),
5680
+ onClick: onToggleHaptics,
5312
5681
  className: `
5313
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5314
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
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: lang.name }),
5318
- isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
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
- lang.code
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
- nostalgistRef.current.restart();
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({ nostalgistRef }) {
7340
- const applyCheat = React2.useCallback((code) => {
7735
+ function useEmulatorCheats({
7736
+ nostalgistRef
7737
+ }) {
7738
+ const allocatedPointersRef = React2.useRef([]);
7739
+ const injectCheats = React2.useCallback((cheats) => {
7341
7740
  if (!nostalgistRef.current) return;
7342
- try {
7343
- nostalgistRef.current.addCheat(code);
7344
- } catch (err) {
7345
- console.error("[Nostalgist] Apply cheat error:", err);
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
- }, [nostalgistRef]);
7348
- const resetCheats = React2.useCallback(() => {
7349
- if (!nostalgistRef.current) return;
7350
- try {
7351
- nostalgistRef.current.resetCheats();
7352
- } catch (err) {
7353
- console.error("[Nostalgist] Reset cheats error:", err);
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
- applyCheat,
7358
- resetCheats
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
- applyCheat,
7454
- resetCheats
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
- applyCheat,
7499
- resetCheats,
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
- applyCheat,
7531
- resetCheats,
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?.(cheatId, !isActive);
8280
- nostalgist.resetCheats();
8281
- setTimeout(() => {
8282
- newActiveCheats.forEach((id) => {
8283
- const cheat = cheats.find((c) => c.id === id);
8284
- if (cheat) {
8285
- nostalgist.applyCheat(cheat.code);
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
- handleToggleCheat
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
- handleToggleCheat
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", cheats = [], onExit } = props;
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(