koin.js 1.0.16 → 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",
@@ -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,48 +2962,364 @@ 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,
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,
2995
3323
  // RT / R2
2996
3324
  // System
2997
3325
  select: 8,
@@ -3541,351 +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 if (onPositionChange) {
3711
- const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
3712
- if (!startedDrag) {
3713
- drag.clearDragTimer();
3714
- const rect = dpadRef.current?.getBoundingClientRect();
3715
- if (rect) {
3716
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3717
- }
3718
- }
3719
- } else {
3720
- const rect = dpadRef.current?.getBoundingClientRect();
3721
- if (rect) {
3722
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3723
- }
3724
- }
3725
- }, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
3726
- const handleTouchEnd = React2.useCallback((e) => {
3727
- e.preventDefault();
3728
- drag.clearDragTimer();
3729
- let touchEnded = false;
3730
- for (let i = 0; i < e.changedTouches.length; i++) {
3731
- if (e.changedTouches[i].identifier === activeTouchRef.current) {
3732
- touchEnded = true;
3733
- break;
3734
- }
3735
- }
3736
- if (touchEnded) {
3737
- activeTouchRef.current = null;
3738
- if (drag.isDragging) {
3739
- drag.handleDragEnd();
3740
- } else {
3741
- activeDirectionsRef.current.forEach((dir) => {
3742
- const keyCode = getKeyCode(dir);
3743
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3744
- });
3745
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3746
- updateVisuals(/* @__PURE__ */ new Set());
3747
- }
3748
- }
3749
- }, [getKeyCode, updateVisuals, drag]);
3750
- useTouchEvents(dpadRef, {
3751
- onTouchStart: handleTouchStart,
3752
- onTouchMove: handleTouchMove,
3753
- onTouchEnd: handleTouchEnd,
3754
- onTouchCancel: handleTouchEnd
3755
- }, { cleanup: drag.clearDragTimer });
3756
- const leftPx = displayX / 100 * containerWidth - size / 2;
3757
- const topPx = displayY / 100 * containerHeight - size / 2;
3758
- const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
3759
- const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
3760
- const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
3761
- const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
3762
- return /* @__PURE__ */ jsxRuntime.jsxs(
3763
- "div",
3764
- {
3765
- ref: dpadRef,
3766
- className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3767
- style: {
3768
- top: 0,
3769
- left: 0,
3770
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3771
- width: size,
3772
- height: size,
3773
- opacity: isLandscape ? 0.75 : 0.9,
3774
- WebkitTouchCallout: "none",
3775
- WebkitUserSelect: "none",
3776
- touchAction: "none",
3777
- transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3778
- },
3779
- children: [
3780
- /* @__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"}` }),
3781
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3782
- /* @__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" }),
3783
- /* @__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" }),
3784
- /* @__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" }),
3785
- /* @__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" }),
3786
- /* @__PURE__ */ jsxRuntime.jsx(
3787
- "circle",
3788
- {
3789
- ref: centerCircleRef,
3790
- cx: "50",
3791
- cy: "50",
3792
- r: "12",
3793
- fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3794
- stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3795
- strokeWidth: drag.isDragging ? 2 : 1
3796
- }
3797
- ),
3798
- /* @__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" }),
3799
- /* @__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" }),
3800
- /* @__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" }),
3801
- /* @__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" })
3802
- ] })
3803
- ]
3804
- }
3805
- );
3806
- });
3807
- var Dpad_default = Dpad;
3808
-
3809
- // src/components/VirtualController/positioning.ts
3810
- function adjustButtonPosition(config, context) {
3811
- const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
3812
- return {
3813
- ...config,
3814
- size: Math.floor(config.size * sizeMultiplier)
3815
- };
3816
- }
3817
- var STORAGE_KEY = "virtual-button-positions";
3818
- function useButtonPositions() {
3819
- const [landscapePositions, setLandscapePositions] = React2.useState({});
3820
- const [portraitPositions, setPortraitPositions] = React2.useState({});
3821
- React2.useEffect(() => {
3822
- try {
3823
- const stored = localStorage.getItem(STORAGE_KEY);
3824
- if (stored) {
3825
- const parsed = JSON.parse(stored);
3826
- if (parsed.landscape && parsed.portrait) {
3827
- setLandscapePositions(parsed.landscape);
3828
- setPortraitPositions(parsed.portrait);
3829
- } else {
3830
- setLandscapePositions(parsed);
3831
- }
3832
- }
3833
- } catch (e) {
3834
- console.error("Failed to load button positions:", e);
3835
- }
3836
- }, []);
3837
- const savePosition = React2.useCallback((buttonType, x, y, isLandscape) => {
3838
- if (isLandscape) {
3839
- setLandscapePositions((prev) => {
3840
- const updated = { ...prev, [buttonType]: { x, y } };
3841
- try {
3842
- const stored = {
3843
- landscape: updated,
3844
- portrait: portraitPositions
3845
- };
3846
- localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3847
- } catch (e) {
3848
- console.error("Failed to save button position:", e);
3849
- }
3850
- return updated;
3851
- });
3852
- } else {
3853
- setPortraitPositions((prev) => {
3854
- const updated = { ...prev, [buttonType]: { x, y } };
3855
- try {
3856
- const stored = {
3857
- landscape: landscapePositions,
3858
- portrait: updated
3859
- };
3860
- localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3861
- } catch (e) {
3862
- console.error("Failed to save button position:", e);
3863
- }
3864
- return updated;
3865
- });
3866
- }
3867
- }, [landscapePositions, portraitPositions]);
3868
- const getPosition = React2.useCallback((buttonType, isLandscape) => {
3869
- const positions = isLandscape ? landscapePositions : portraitPositions;
3870
- return positions[buttonType] || null;
3871
- }, [landscapePositions, portraitPositions]);
3872
- const resetPositions = React2.useCallback(() => {
3873
- setLandscapePositions({});
3874
- setPortraitPositions({});
3875
- try {
3876
- localStorage.removeItem(STORAGE_KEY);
3877
- } catch (e) {
3878
- console.error("Failed to reset button positions:", e);
3879
- }
3880
- }, []);
3881
- return {
3882
- landscapePositions,
3883
- portraitPositions,
3884
- savePosition,
3885
- getPosition,
3886
- resetPositions
3887
- };
3888
- }
3889
3872
  var STORAGE_KEY2 = "koin-controls-hint-shown";
3890
3873
  function ControlsHint({ isVisible }) {
3891
3874
  const [show, setShow] = React2.useState(false);
@@ -3970,7 +3953,7 @@ function LockButton({
3970
3953
  "button",
3971
3954
  {
3972
3955
  onClick: onToggle,
3973
- 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",
3974
3957
  style: {
3975
3958
  backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3976
3959
  border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
@@ -3984,18 +3967,164 @@ function LockButton({
3984
3967
  }
3985
3968
  )
3986
3969
  }
3987
- );
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
+ ) });
3988
4109
  }
3989
4110
  var LOCK_KEY = "koin-controls-locked";
3990
4111
  function VirtualController({
3991
4112
  system,
3992
4113
  isRunning,
3993
4114
  controls,
3994
- systemColor = "#00FF41"
4115
+ systemColor = "#00FF41",
3995
4116
  // Default retro green
4117
+ hapticsEnabled = true,
4118
+ onButtonDown,
4119
+ onButtonUp,
4120
+ onPause,
4121
+ onResume
3996
4122
  }) {
3997
4123
  const { isMobile, isLandscape, isPortrait } = useMobile();
3998
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);
3999
4128
  const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
4000
4129
  const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
4001
4130
  const [isLocked, setIsLocked] = React2.useState(true);
@@ -4013,6 +4142,28 @@ function VirtualController({
4013
4142
  return newValue;
4014
4143
  });
4015
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]);
4016
4167
  const layout = getLayoutForSystem(system);
4017
4168
  const visibleButtons = layout.buttons.filter((btn) => {
4018
4169
  if (isPortrait) {
@@ -4082,9 +4233,9 @@ function VirtualController({
4082
4233
  return;
4083
4234
  }
4084
4235
  setPressedButtons((prev) => new Set(prev).add(buttonType));
4085
- dispatchKeyboardEvent("keydown", keyboardCode);
4236
+ onButtonDown(buttonType);
4086
4237
  setTimeout(() => {
4087
- dispatchKeyboardEvent("keyup", keyboardCode);
4238
+ onButtonUp(buttonType);
4088
4239
  setPressedButtons((prev) => {
4089
4240
  const next = new Set(prev);
4090
4241
  next.delete(buttonType);
@@ -4092,7 +4243,7 @@ function VirtualController({
4092
4243
  });
4093
4244
  }, 100);
4094
4245
  },
4095
- [isRunning, getButtonKeyboardCode]
4246
+ [isRunning, getButtonKeyboardCode, onButtonDown, onButtonUp]
4096
4247
  );
4097
4248
  const handlePressDown = React2.useCallback(
4098
4249
  (buttonType) => {
@@ -4101,15 +4252,70 @@ function VirtualController({
4101
4252
  if (isSystemButton) return;
4102
4253
  const keyboardCode = getButtonKeyboardCode(buttonType);
4103
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
+ }
4104
4308
  setPressedButtons((prev) => {
4105
4309
  if (prev.has(buttonType)) return prev;
4106
4310
  const next = new Set(prev);
4107
4311
  next.add(buttonType);
4108
4312
  return next;
4109
4313
  });
4110
- dispatchKeyboardEvent("keydown", keyboardCode);
4314
+ if (!heldButtons.has(buttonType)) {
4315
+ onButtonDown(buttonType);
4316
+ }
4111
4317
  },
4112
- [isRunning, getButtonKeyboardCode]
4318
+ [isRunning, getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonDown, onButtonUp]
4113
4319
  );
4114
4320
  const handleRelease = React2.useCallback(
4115
4321
  (buttonType) => {
@@ -4117,15 +4323,29 @@ function VirtualController({
4117
4323
  if (isSystemButton) return;
4118
4324
  const keyboardCode = getButtonKeyboardCode(buttonType);
4119
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;
4120
4340
  setPressedButtons((prev) => {
4121
4341
  if (!prev.has(buttonType)) return prev;
4122
4342
  const next = new Set(prev);
4123
4343
  next.delete(buttonType);
4124
4344
  return next;
4125
4345
  });
4126
- dispatchKeyboardEvent("keyup", keyboardCode);
4346
+ onButtonUp(buttonType);
4127
4347
  },
4128
- [getButtonKeyboardCode]
4348
+ [getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonUp]
4129
4349
  );
4130
4350
  React2.useEffect(() => {
4131
4351
  if (!isRunning && pressedButtons.size > 0) {
@@ -4137,6 +4357,22 @@ function VirtualController({
4137
4357
  setPressedButtons(/* @__PURE__ */ new Set());
4138
4358
  }
4139
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]);
4140
4376
  const memoizedButtonElements = React2.useMemo(() => {
4141
4377
  const width = containerSize.width || (typeof window !== "undefined" ? window.innerWidth : 0);
4142
4378
  const height = containerSize.height || (typeof window !== "undefined" ? window.innerHeight : 0);
@@ -4167,14 +4403,32 @@ function VirtualController({
4167
4403
  className: "fixed inset-0 z-30 pointer-events-none",
4168
4404
  style: { touchAction: "none" },
4169
4405
  children: [
4170
- /* @__PURE__ */ jsxRuntime.jsx(
4171
- LockButton,
4172
- {
4173
- isLocked,
4174
- onToggle: toggleLock,
4175
- systemColor
4176
- }
4177
- ),
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
+ ] }),
4178
4432
  /* @__PURE__ */ jsxRuntime.jsx(
4179
4433
  Dpad_default,
4180
4434
  {
@@ -4183,18 +4437,20 @@ function VirtualController({
4183
4437
  y: finalDpadY,
4184
4438
  containerWidth: containerSize.width || window.innerWidth,
4185
4439
  containerHeight: containerSize.height || window.innerHeight,
4186
- controls,
4187
4440
  systemColor,
4188
4441
  isLandscape,
4189
4442
  customPosition: getPosition("up", isLandscape),
4190
- 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
4191
4447
  }
4192
4448
  ),
4193
4449
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
4194
4450
  VirtualButton_default,
4195
4451
  {
4196
4452
  config: adjustedConfig,
4197
- isPressed: pressedButtons.has(buttonConfig.type),
4453
+ isPressed: pressedButtons.has(buttonConfig.type) || heldButtons.has(buttonConfig.type),
4198
4454
  onPress: handlePress,
4199
4455
  onPressDown: handlePressDown,
4200
4456
  onRelease: handleRelease,
@@ -4203,10 +4459,24 @@ function VirtualController({
4203
4459
  customPosition,
4204
4460
  onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4205
4461
  isLandscape,
4206
- 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)
4207
4467
  },
4208
4468
  buttonConfig.type
4209
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
+ ),
4210
4480
  /* @__PURE__ */ jsxRuntime.jsx(ControlsHint, { isVisible: isRunning })
4211
4481
  ]
4212
4482
  }
@@ -4893,7 +5163,8 @@ function CheatModal({
4893
5163
  cheats,
4894
5164
  activeCheats,
4895
5165
  onToggle,
4896
- onClose
5166
+ onClose,
5167
+ onAddManualCheat
4897
5168
  }) {
4898
5169
  const t = useKoinTranslation();
4899
5170
  const [copiedId, setCopiedId] = React2__default.default.useState(null);
@@ -4911,54 +5182,111 @@ function CheatModal({
4911
5182
  subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4912
5183
  icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
4913
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 }),
4914
- 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: [
4915
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4916
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4917
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
4918
- ] }) : cheats.map((cheat) => {
4919
- const isActive = activeCheats.has(cheat.id);
4920
- return /* @__PURE__ */ jsxRuntime.jsxs(
4921
- "div",
4922
- {
4923
- className: `
4924
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4925
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4926
- `,
4927
- onClick: () => onToggle(cheat.id),
4928
- children: [
4929
- /* @__PURE__ */ jsxRuntime.jsx(
4930
- "div",
4931
- {
4932
- className: `
4933
- flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4934
- ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4935
- `,
4936
- 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
+ }
4937
5204
  }
4938
- ),
4939
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4940
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description }),
4941
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
4942
- /* @__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 }),
4943
- /* @__PURE__ */ jsxRuntime.jsx(
4944
- "button",
4945
- {
4946
- onClick: (e) => {
4947
- e.stopPropagation();
4948
- handleCopy(cheat.code, cheat.id);
4949
- },
4950
- className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
4951
- title: t.modals.cheats.copy,
4952
- children: copiedId === cheat.id ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { size: 14 })
4953
- }
4954
- )
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
+ ] })
4955
5283
  ] })
4956
- ] })
4957
- ]
4958
- },
4959
- cheat.id
4960
- );
4961
- }) })
5284
+ ]
5285
+ },
5286
+ cheat.id
5287
+ );
5288
+ }) })
5289
+ ] })
4962
5290
  }
4963
5291
  );
4964
5292
  }
@@ -5290,7 +5618,9 @@ function SettingsModal({
5290
5618
  onClose,
5291
5619
  currentLanguage,
5292
5620
  onLanguageChange,
5293
- systemColor = "#00FF41"
5621
+ systemColor = "#00FF41",
5622
+ hapticsEnabled,
5623
+ onToggleHaptics
5294
5624
  }) {
5295
5625
  const t = useKoinTranslation();
5296
5626
  const languages = [
@@ -5314,30 +5644,57 @@ function SettingsModal({
5314
5644
  children: t.modals.shortcuts.pressEsc
5315
5645
  }
5316
5646
  ),
5317
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
5318
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5319
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
5320
- /* @__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
+ }) })
5321
5671
  ] }),
5322
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
5323
- const isActive = currentLanguage === lang.code;
5324
- 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(
5325
5678
  "button",
5326
5679
  {
5327
- onClick: () => onLanguageChange(lang.code),
5680
+ onClick: onToggleHaptics,
5328
5681
  className: `
5329
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5330
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5331
- `,
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
+ `,
5332
5685
  children: [
5333
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
5334
- 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
+ ) })
5335
5693
  ]
5336
- },
5337
- lang.code
5338
- );
5339
- }) })
5340
- ] }) })
5694
+ }
5695
+ )
5696
+ ] })
5697
+ ] })
5341
5698
  }
5342
5699
  );
5343
5700
  }
@@ -5357,6 +5714,7 @@ function GameModals({
5357
5714
  cheats,
5358
5715
  activeCheats,
5359
5716
  onToggleCheat,
5717
+ onAddManualCheat,
5360
5718
  saveModalOpen,
5361
5719
  setSaveModalOpen,
5362
5720
  saveModalMode,
@@ -5376,7 +5734,9 @@ function GameModals({
5376
5734
  settingsModalOpen,
5377
5735
  setSettingsModalOpen,
5378
5736
  currentLanguage,
5379
- onLanguageChange
5737
+ onLanguageChange,
5738
+ hapticsEnabled,
5739
+ onToggleHaptics
5380
5740
  }) {
5381
5741
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5382
5742
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5411,6 +5771,7 @@ function GameModals({
5411
5771
  cheats,
5412
5772
  activeCheats,
5413
5773
  onToggle: onToggleCheat,
5774
+ onAddManualCheat,
5414
5775
  onClose: () => {
5415
5776
  setCheatsModalOpen(false);
5416
5777
  onResume();
@@ -5463,7 +5824,9 @@ function GameModals({
5463
5824
  },
5464
5825
  currentLanguage,
5465
5826
  onLanguageChange,
5466
- systemColor
5827
+ systemColor,
5828
+ hapticsEnabled,
5829
+ onToggleHaptics
5467
5830
  }
5468
5831
  )
5469
5832
  ] });
@@ -6758,6 +7121,11 @@ function useEmulatorCore({
6758
7121
  input_volume_up: "add",
6759
7122
  input_volume_down: "subtract",
6760
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",
6761
7129
  ...inputConfig,
6762
7130
  ...specificConfig,
6763
7131
  // Apply system-specific optimizations
@@ -6851,18 +7219,15 @@ function useEmulatorCore({
6851
7219
  const restart = React2.useCallback(async () => {
6852
7220
  if (nostalgistRef.current) {
6853
7221
  try {
6854
- nostalgistRef.current.restart();
6855
- await new Promise((resolve) => setTimeout(resolve, 100));
6856
- nostalgistRef.current.resume();
6857
- setIsPaused(false);
6858
- setStatus("running");
6859
- } catch (err) {
6860
- console.error("[Nostalgist] Restart error:", err);
7222
+ console.log("[Nostalgist] Full restart - stopping, re-preparing with fresh config, starting");
6861
7223
  stop();
7224
+ await prepare();
6862
7225
  await start();
7226
+ } catch (err) {
7227
+ console.error("[Nostalgist] Restart error:", err);
6863
7228
  }
6864
7229
  }
6865
- }, [stop, start]);
7230
+ }, [stop, prepare, start]);
6866
7231
  const pause = React2.useCallback(() => {
6867
7232
  if (nostalgistRef.current && !isPaused && status === "running") {
6868
7233
  try {
@@ -6955,9 +7320,6 @@ function useEmulatorCore({
6955
7320
  console.error("[Nostalgist] Resize error:", err);
6956
7321
  }
6957
7322
  }, []);
6958
- React2.useCallback(() => {
6959
- return nostalgistRef.current;
6960
- }, []);
6961
7323
  return {
6962
7324
  status,
6963
7325
  setStatus,
@@ -7084,8 +7446,26 @@ function useEmulatorInput({ nostalgistRef }) {
7084
7446
  console.error("[Nostalgist] Press key error:", err);
7085
7447
  }
7086
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]);
7087
7465
  return {
7088
- pressKey
7466
+ pressKey,
7467
+ pressDown,
7468
+ pressUp
7089
7469
  };
7090
7470
  }
7091
7471
  var MIN_SAVE_INTERVAL = 100;
@@ -7352,26 +7732,53 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
7352
7732
  stopRewindCapture
7353
7733
  };
7354
7734
  }
7355
- function useEmulatorCheats({ nostalgistRef }) {
7356
- const applyCheat = React2.useCallback((code) => {
7735
+ function useEmulatorCheats({
7736
+ nostalgistRef
7737
+ }) {
7738
+ const allocatedPointersRef = React2.useRef([]);
7739
+ const injectCheats = React2.useCallback((cheats) => {
7357
7740
  if (!nostalgistRef.current) return;
7358
- try {
7359
- nostalgistRef.current.addCheat(code);
7360
- } catch (err) {
7361
- 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 = [];
7362
7751
  }
7363
- }, [nostalgistRef]);
7364
- const resetCheats = React2.useCallback(() => {
7365
- if (!nostalgistRef.current) return;
7366
- try {
7367
- nostalgistRef.current.resetCheats();
7368
- } catch (err) {
7369
- 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();
7370
7774
  }
7371
7775
  }, [nostalgistRef]);
7776
+ const clearCheats = React2.useCallback(() => {
7777
+ injectCheats([]);
7778
+ }, [injectCheats]);
7372
7779
  return {
7373
- applyCheat,
7374
- resetCheats
7780
+ injectCheats,
7781
+ clearCheats
7375
7782
  };
7376
7783
  }
7377
7784
 
@@ -7443,7 +7850,9 @@ var useNostalgist = ({
7443
7850
  initialVolume
7444
7851
  });
7445
7852
  const {
7446
- pressKey
7853
+ pressKey,
7854
+ pressDown,
7855
+ pressUp
7447
7856
  } = useEmulatorInput({
7448
7857
  nostalgistRef
7449
7858
  });
@@ -7466,8 +7875,8 @@ var useNostalgist = ({
7466
7875
  // Disable manual rewind loop for heavy systems
7467
7876
  });
7468
7877
  const {
7469
- applyCheat,
7470
- resetCheats
7878
+ injectCheats,
7879
+ clearCheats
7471
7880
  } = useEmulatorCheats({
7472
7881
  nostalgistRef
7473
7882
  });
@@ -7510,9 +7919,11 @@ var useNostalgist = ({
7510
7919
  toggleMute,
7511
7920
  screenshot,
7512
7921
  pressKey,
7922
+ pressDown,
7923
+ pressUp,
7513
7924
  resize,
7514
- applyCheat,
7515
- resetCheats,
7925
+ injectCheats,
7926
+ clearCheats,
7516
7927
  getNostalgistInstance,
7517
7928
  isPerformanceMode
7518
7929
  }), [
@@ -7542,9 +7953,11 @@ var useNostalgist = ({
7542
7953
  toggleMute,
7543
7954
  screenshot,
7544
7955
  pressKey,
7956
+ pressDown,
7957
+ pressUp,
7545
7958
  resize,
7546
- applyCheat,
7547
- resetCheats,
7959
+ injectCheats,
7960
+ clearCheats,
7548
7961
  getNostalgistInstance,
7549
7962
  isPerformanceMode
7550
7963
  ]);
@@ -8275,39 +8688,95 @@ function useGameSaves({
8275
8688
  handleAutoSaveToggle
8276
8689
  };
8277
8690
  }
8691
+ var generateManualId = () => `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8278
8692
  function useGameCheats({
8279
8693
  nostalgist,
8280
8694
  cheats = [],
8281
- onToggleCheat
8695
+ onToggleCheat,
8696
+ showToast,
8697
+ romId
8282
8698
  }) {
8283
8699
  const [cheatsModalOpen, setCheatsModalOpen] = React2.useState(false);
8284
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
+ };
8285
8751
  const handleToggleCheat = (cheatId) => {
8286
8752
  if (!nostalgist) return;
8287
8753
  const newActiveCheats = new Set(activeCheats);
8288
8754
  const isActive = newActiveCheats.has(cheatId);
8289
8755
  if (isActive) {
8290
8756
  newActiveCheats.delete(cheatId);
8757
+ showToast?.("Cheat Disabled");
8291
8758
  } else {
8292
8759
  newActiveCheats.add(cheatId);
8760
+ showToast?.("Cheat Enabled", "success");
8293
8761
  }
8294
8762
  setActiveCheats(newActiveCheats);
8295
- onToggleCheat?.(cheatId, !isActive);
8296
- nostalgist.resetCheats();
8297
- setTimeout(() => {
8298
- newActiveCheats.forEach((id) => {
8299
- const cheat = cheats.find((c) => c.id === id);
8300
- if (cheat) {
8301
- nostalgist.applyCheat(cheat.code);
8302
- }
8303
- });
8304
- }, 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);
8305
8771
  };
8306
8772
  return {
8307
8773
  cheatsModalOpen,
8308
8774
  setCheatsModalOpen,
8309
8775
  activeCheats,
8310
- handleToggleCheat
8776
+ allCheats,
8777
+ // Unified list - replaces separate cheats + manualCheats
8778
+ handleToggleCheat,
8779
+ handleAddManualCheat
8311
8780
  };
8312
8781
  }
8313
8782
  function useGameRecording({
@@ -8484,10 +8953,13 @@ function useGamePlayer(props) {
8484
8953
  cheatsModalOpen,
8485
8954
  setCheatsModalOpen,
8486
8955
  activeCheats,
8487
- handleToggleCheat
8956
+ allCheats,
8957
+ handleToggleCheat,
8958
+ handleAddManualCheat
8488
8959
  } = useGameCheats({
8489
8960
  ...props,
8490
- nostalgist
8961
+ nostalgist,
8962
+ showToast
8491
8963
  });
8492
8964
  const {
8493
8965
  isRecording,
@@ -8543,7 +9015,9 @@ function useGamePlayer(props) {
8543
9015
  hardcoreRestrictions,
8544
9016
  // Cheats
8545
9017
  activeCheats,
9018
+ allCheats,
8546
9019
  handleToggleCheat,
9020
+ handleAddManualCheat,
8547
9021
  // Emulator
8548
9022
  nostalgist,
8549
9023
  volumeState,
@@ -8571,7 +9045,8 @@ var DEFAULT_SETTINGS = {
8571
9045
  muted: false,
8572
9046
  shader: "",
8573
9047
  showPerformanceOverlay: false,
8574
- showInputDisplay: false
9048
+ showInputDisplay: false,
9049
+ hapticsEnabled: true
8575
9050
  };
8576
9051
  function usePlayerPersistence(onSettingsChange) {
8577
9052
  const [settings, setSettings] = React2.useState(DEFAULT_SETTINGS);
@@ -8678,7 +9153,9 @@ var es = {
8678
9153
  shortcuts: "Atajos",
8679
9154
  exit: "Salir",
8680
9155
  language: "Idioma",
8681
- selectLanguage: "Seleccionar idioma"
9156
+ selectLanguage: "Seleccionar idioma",
9157
+ haptics: "Vibraci\xF3n",
9158
+ enableHaptics: "Habilitar vibraci\xF3n"
8682
9159
  },
8683
9160
  overlay: {
8684
9161
  play: "JUGAR",
@@ -8761,7 +9238,10 @@ var es = {
8761
9238
  emptyDesc: "No se encontraron c\xF3digos para este juego",
8762
9239
  copy: "Copiar c\xF3digo",
8763
9240
  active: "{{count}} truco{{s}} activo(s)",
8764
- 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"
8765
9245
  },
8766
9246
  saveSlots: {
8767
9247
  title: "Guardar partida",
@@ -8906,7 +9386,9 @@ var fr = {
8906
9386
  shortcuts: "Raccourcis",
8907
9387
  exit: "Quitter",
8908
9388
  language: "Langue",
8909
- selectLanguage: "Choisir la langue"
9389
+ selectLanguage: "Choisir la langue",
9390
+ haptics: "Vibration",
9391
+ enableHaptics: "Activer la vibration"
8910
9392
  },
8911
9393
  overlay: {
8912
9394
  play: "JOUER",
@@ -8989,7 +9471,10 @@ var fr = {
8989
9471
  emptyDesc: "Aucun code trouv\xE9 pour ce jeu",
8990
9472
  copy: "Copier",
8991
9473
  active: "{{count}} actif(s)",
8992
- 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"
8993
9478
  },
8994
9479
  saveSlots: {
8995
9480
  title: "Sauvegardes",
@@ -9162,7 +9647,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9162
9647
  hardcoreRestrictions,
9163
9648
  // Cheats
9164
9649
  activeCheats,
9650
+ allCheats,
9165
9651
  handleToggleCheat,
9652
+ handleAddManualCheat,
9166
9653
  // Emulator Instance & State
9167
9654
  nostalgist,
9168
9655
  // Contains start, restart, etc.
@@ -9227,7 +9714,7 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9227
9714
  if (muted !== settings.muted) toggleMute();
9228
9715
  }
9229
9716
  }, [settingsLoaded]);
9230
- const { system, systemColor = "#00FF41", cheats = [], onExit } = props;
9717
+ const { system, systemColor = "#00FF41", onExit } = props;
9231
9718
  const handlePauseToggle = React2.useCallback(() => {
9232
9719
  status === "ready" ? start() : togglePause();
9233
9720
  }, [status, start, togglePause]);
@@ -9283,6 +9770,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9283
9770
  const handleToggleInputDisplay = React2.useCallback(() => {
9284
9771
  updateSettings({ showInputDisplay: !settings.showInputDisplay });
9285
9772
  }, [updateSettings, settings.showInputDisplay]);
9773
+ const handleToggleHaptics = React2.useCallback(() => {
9774
+ updateSettings({ hapticsEnabled: !settings.hapticsEnabled });
9775
+ }, [updateSettings, settings.hapticsEnabled]);
9286
9776
  const handleToggleRecording = React2.useCallback(async () => {
9287
9777
  if (!recordingSupported) {
9288
9778
  console.warn("[Recording] Not supported in this browser");
@@ -9369,7 +9859,12 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9369
9859
  system,
9370
9860
  isRunning: status === "running" || status === "paused",
9371
9861
  controls,
9372
- systemColor
9862
+ systemColor,
9863
+ hapticsEnabled: settings.hapticsEnabled,
9864
+ onButtonDown: nostalgist.pressDown,
9865
+ onButtonUp: nostalgist.pressUp,
9866
+ onPause: nostalgist.pause,
9867
+ onResume: nostalgist.resume
9373
9868
  }
9374
9869
  ),
9375
9870
  !isFullscreen2 && isMobile && /* @__PURE__ */ jsxRuntime.jsx(
@@ -9524,9 +10019,10 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9524
10019
  systemColor,
9525
10020
  cheatsModalOpen,
9526
10021
  setCheatsModalOpen,
9527
- cheats,
10022
+ cheats: allCheats,
9528
10023
  activeCheats,
9529
10024
  onToggleCheat: handleToggleCheat,
10025
+ onAddManualCheat: handleAddManualCheat,
9530
10026
  saveModalOpen,
9531
10027
  setSaveModalOpen,
9532
10028
  saveModalMode,
@@ -9546,7 +10042,9 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
9546
10042
  settingsModalOpen,
9547
10043
  setSettingsModalOpen,
9548
10044
  currentLanguage: props.currentLanguage,
9549
- onLanguageChange: props.onLanguageChange
10045
+ onLanguageChange: props.onLanguageChange,
10046
+ hapticsEnabled: settings.hapticsEnabled,
10047
+ onToggleHaptics: handleToggleHaptics
9550
10048
  }
9551
10049
  ),
9552
10050
  !isMobile && /* @__PURE__ */ jsxRuntime.jsx(