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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import React2, { memo, useState, useEffect, useRef, createContext, useMemo, useCallback, useContext } from 'react';
2
- import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, X, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Lock, Unlock, Move, User, Copy, Zap, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
2
+ import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, Hand, Zap, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, X, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Lock, Unlock, StopCircle, ZapOff, Move, User, Copy, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { Nostalgist } from 'nostalgist';
@@ -328,9 +328,11 @@ var en = {
328
328
  cheats: "Cheats",
329
329
  retroAchievements: "RetroAchievements",
330
330
  shortcuts: "Shortcuts",
331
- exit: "Exit",
331
+ exit: "Exit Game",
332
332
  language: "Language",
333
- selectLanguage: "Select Language"
333
+ selectLanguage: "Select Language",
334
+ haptics: "Haptic Feedback",
335
+ enableHaptics: "Vibration"
334
336
  },
335
337
  overlay: {
336
338
  play: "PLAY",
@@ -407,13 +409,16 @@ var en = {
407
409
  },
408
410
  cheats: {
409
411
  title: "Cheats",
410
- addCheat: "Add Cheat",
411
- available: "{{count}} cheat{{s}} available",
412
- emptyTitle: "No cheats available",
413
- emptyDesc: "No cheat codes found for this game",
414
- copy: "Copy code",
415
- active: "{{count}} cheat{{s}} active",
416
- toggleHint: "Click a cheat to toggle it on/off"
412
+ addCheat: "Add Manual Cheat",
413
+ available: "Available Cheats",
414
+ emptyTitle: "No Cheats Available",
415
+ emptyDesc: "This game does not have any known cheats.",
416
+ copy: "Copy",
417
+ active: "Active",
418
+ toggleHint: "Click to toggle",
419
+ codePlaceholder: "Enter cheat code (e.g. 00C-048-E6E)",
420
+ descPlaceholder: "Cheat description (optional)",
421
+ add: "Add Cheat"
417
422
  },
418
423
  saveSlots: {
419
424
  title: "Save States",
@@ -2586,7 +2591,8 @@ function useTouchHandlers({
2586
2591
  onPress,
2587
2592
  onPressDown,
2588
2593
  onRelease,
2589
- onPositionChange
2594
+ onPositionChange,
2595
+ hapticsEnabled = true
2590
2596
  }) {
2591
2597
  const isDraggingRef = useRef(false);
2592
2598
  const drag = useDrag({
@@ -2612,7 +2618,7 @@ function useTouchHandlers({
2612
2618
  const touch = e.touches[0];
2613
2619
  e.preventDefault();
2614
2620
  e.stopPropagation();
2615
- if (navigator.vibrate) {
2621
+ if (hapticsEnabled && navigator.vibrate) {
2616
2622
  navigator.vibrate(8);
2617
2623
  }
2618
2624
  if (isSystemButton) {
@@ -2849,7 +2855,11 @@ var VirtualButton = React2.memo(function VirtualButton2({
2849
2855
  customPosition,
2850
2856
  onPositionChange,
2851
2857
  isLandscape = false,
2852
- console: console2 = ""
2858
+ console: console2 = "",
2859
+ hapticsEnabled = true,
2860
+ mode = "normal",
2861
+ isHeld = false,
2862
+ isInTurbo = false
2853
2863
  }) {
2854
2864
  const t = useKoinTranslation();
2855
2865
  const buttonRef = useRef(null);
@@ -2876,7 +2886,9 @@ var VirtualButton = React2.memo(function VirtualButton2({
2876
2886
  onPress,
2877
2887
  onPressDown,
2878
2888
  onRelease,
2879
- onPositionChange
2889
+ onPositionChange,
2890
+ hapticsEnabled
2891
+ // Pass haptics setting
2880
2892
  });
2881
2893
  useTouchEvents(buttonRef, {
2882
2894
  onTouchStart: handleTouchStart,
@@ -2904,7 +2916,7 @@ var VirtualButton = React2.memo(function VirtualButton2({
2904
2916
  width = `${config.size * 2}px`;
2905
2917
  }
2906
2918
  }
2907
- return /* @__PURE__ */ jsx(
2919
+ return /* @__PURE__ */ jsxs(
2908
2920
  "button",
2909
2921
  {
2910
2922
  ref: buttonRef,
@@ -2944,48 +2956,364 @@ var VirtualButton = React2.memo(function VirtualButton2({
2944
2956
  },
2945
2957
  "aria-label": label,
2946
2958
  onContextMenu: (e) => e.preventDefault(),
2947
- children: /* @__PURE__ */ jsx("span", { className: "drop-shadow-md", children: label })
2959
+ children: [
2960
+ (mode !== "normal" || isHeld || isInTurbo) && /* @__PURE__ */ jsx(
2961
+ "span",
2962
+ {
2963
+ className: "absolute -top-1 -right-1 rounded-full flex items-center justify-center",
2964
+ style: {
2965
+ width: "16px",
2966
+ height: "16px",
2967
+ backgroundColor: isHeld ? "#22c55e" : isInTurbo ? "#fbbf24" : "rgba(255,255,255,0.2)",
2968
+ animation: isInTurbo ? "pulse 0.5s infinite" : "none"
2969
+ },
2970
+ children: isHeld ? /* @__PURE__ */ jsx(Hand, { size: 10, color: "white", strokeWidth: 3 }) : isInTurbo ? /* @__PURE__ */ jsx(Zap, { size: 10, color: "white", fill: "white" }) : mode === "hold" ? /* @__PURE__ */ jsx(Hand, { size: 10, color: "white" }) : mode === "turbo" ? /* @__PURE__ */ jsx(Zap, { size: 10, color: "white" }) : null
2971
+ }
2972
+ ),
2973
+ /* @__PURE__ */ jsx("span", { className: "drop-shadow-md", children: label })
2974
+ ]
2948
2975
  }
2949
2976
  );
2950
2977
  });
2951
2978
  var VirtualButton_default = VirtualButton;
2979
+ var CENTER_TOUCH_RADIUS = 0.35;
2980
+ var Dpad = React2.memo(function Dpad2({
2981
+ size = 180,
2982
+ x,
2983
+ y,
2984
+ containerWidth,
2985
+ containerHeight,
2986
+ systemColor = "#00FF41",
2987
+ isLandscape = false,
2988
+ customPosition,
2989
+ onPositionChange,
2990
+ hapticsEnabled = true,
2991
+ onButtonDown,
2992
+ onButtonUp
2993
+ }) {
2994
+ const dpadRef = useRef(null);
2995
+ const activeTouchRef = useRef(null);
2996
+ const activeDirectionsRef = useRef(/* @__PURE__ */ new Set());
2997
+ const upPathRef = useRef(null);
2998
+ const downPathRef = useRef(null);
2999
+ const leftPathRef = useRef(null);
3000
+ const rightPathRef = useRef(null);
3001
+ const centerCircleRef = useRef(null);
3002
+ const displayX = customPosition ? customPosition.x : x;
3003
+ const displayY = customPosition ? customPosition.y : y;
3004
+ const releaseAllDirections = useCallback(() => {
3005
+ activeDirectionsRef.current.forEach((dir) => onButtonUp(dir));
3006
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3007
+ }, [onButtonUp]);
3008
+ const drag = useDrag({
3009
+ elementSize: size,
3010
+ displayX,
3011
+ displayY,
3012
+ containerWidth,
3013
+ containerHeight,
3014
+ onPositionChange,
3015
+ centerThreshold: CENTER_TOUCH_RADIUS,
3016
+ onDragStart: () => {
3017
+ releaseAllDirections();
3018
+ updateVisuals(/* @__PURE__ */ new Set());
3019
+ }
3020
+ });
3021
+ const getDirectionsFromTouch = useCallback((touchX, touchY, rect) => {
3022
+ const centerX = rect.left + rect.width / 2;
3023
+ const centerY = rect.top + rect.height / 2;
3024
+ const dx = touchX - centerX;
3025
+ const dy = touchY - centerY;
3026
+ const distance = Math.sqrt(dx * dx + dy * dy);
3027
+ const deadZone = rect.width / 2 * 0.15;
3028
+ if (distance < deadZone) return /* @__PURE__ */ new Set();
3029
+ const directions = /* @__PURE__ */ new Set();
3030
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI);
3031
+ if (angle >= -150 && angle <= -30) directions.add("up");
3032
+ if (angle >= 30 && angle <= 150) directions.add("down");
3033
+ if (angle >= 120 || angle <= -120) directions.add("left");
3034
+ if (angle >= -60 && angle <= 60) directions.add("right");
3035
+ return directions;
3036
+ }, []);
3037
+ const updateVisuals = useCallback((directions) => {
3038
+ const activeFill = `${systemColor}80`;
3039
+ const inactiveFill = "rgba(255, 255, 255, 0.05)";
3040
+ const activeStroke = systemColor;
3041
+ const inactiveStroke = "rgba(255, 255, 255, 0.2)";
3042
+ const glow = `0 0 15px ${systemColor}`;
3043
+ const updatePart = (ref, isActive) => {
3044
+ if (ref.current) {
3045
+ ref.current.style.fill = isActive ? activeFill : inactiveFill;
3046
+ ref.current.style.stroke = isActive ? activeStroke : inactiveStroke;
3047
+ ref.current.style.filter = isActive ? `drop-shadow(${glow})` : "none";
3048
+ ref.current.style.transform = isActive ? "scale(0.98)" : "scale(1)";
3049
+ ref.current.style.transformOrigin = "center";
3050
+ }
3051
+ };
3052
+ updatePart(upPathRef, directions.has("up"));
3053
+ updatePart(downPathRef, directions.has("down"));
3054
+ updatePart(leftPathRef, directions.has("left"));
3055
+ updatePart(rightPathRef, directions.has("right"));
3056
+ if (centerCircleRef.current) {
3057
+ const isAny = directions.size > 0;
3058
+ centerCircleRef.current.style.fill = isAny ? systemColor : "rgba(0,0,0,0.5)";
3059
+ centerCircleRef.current.style.stroke = isAny ? "#fff" : "rgba(255,255,255,0.3)";
3060
+ }
3061
+ }, [systemColor]);
3062
+ const updateDirections = useCallback((newDirections) => {
3063
+ const prev = activeDirectionsRef.current;
3064
+ prev.forEach((dir) => {
3065
+ if (!newDirections.has(dir)) {
3066
+ onButtonUp(dir);
3067
+ }
3068
+ });
3069
+ newDirections.forEach((dir) => {
3070
+ if (!prev.has(dir)) {
3071
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate(5);
3072
+ onButtonDown(dir);
3073
+ }
3074
+ });
3075
+ activeDirectionsRef.current = newDirections;
3076
+ updateVisuals(newDirections);
3077
+ }, [updateVisuals, onButtonDown, onButtonUp, hapticsEnabled]);
3078
+ const handleTouchStart = useCallback((e) => {
3079
+ e.preventDefault();
3080
+ if (activeTouchRef.current !== null) return;
3081
+ const touch = e.changedTouches[0];
3082
+ activeTouchRef.current = touch.identifier;
3083
+ const rect = dpadRef.current?.getBoundingClientRect();
3084
+ if (!rect) return;
3085
+ if (onPositionChange) {
3086
+ drag.checkDragStart(touch.clientX, touch.clientY, rect);
3087
+ }
3088
+ if (!drag.isDragging) {
3089
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3090
+ }
3091
+ }, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
3092
+ const handleTouchMove = useCallback((e) => {
3093
+ e.preventDefault();
3094
+ let touch = null;
3095
+ for (let i = 0; i < e.changedTouches.length; i++) {
3096
+ if (e.changedTouches[i].identifier === activeTouchRef.current) {
3097
+ touch = e.changedTouches[i];
3098
+ break;
3099
+ }
3100
+ }
3101
+ if (!touch) return;
3102
+ if (drag.isDragging) {
3103
+ drag.handleDragMove(touch.clientX, touch.clientY);
3104
+ } else if (onPositionChange) {
3105
+ const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
3106
+ if (!startedDrag) {
3107
+ drag.clearDragTimer();
3108
+ const rect = dpadRef.current?.getBoundingClientRect();
3109
+ if (rect) {
3110
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3111
+ }
3112
+ }
3113
+ } else {
3114
+ const rect = dpadRef.current?.getBoundingClientRect();
3115
+ if (rect) {
3116
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3117
+ }
3118
+ }
3119
+ }, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
3120
+ const handleTouchEnd = useCallback((e) => {
3121
+ e.preventDefault();
3122
+ drag.clearDragTimer();
3123
+ let touchEnded = false;
3124
+ for (let i = 0; i < e.changedTouches.length; i++) {
3125
+ if (e.changedTouches[i].identifier === activeTouchRef.current) {
3126
+ touchEnded = true;
3127
+ break;
3128
+ }
3129
+ }
3130
+ if (touchEnded) {
3131
+ activeTouchRef.current = null;
3132
+ if (drag.isDragging) {
3133
+ drag.handleDragEnd();
3134
+ } else {
3135
+ activeDirectionsRef.current.forEach((dir) => onButtonUp(dir));
3136
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3137
+ updateVisuals(/* @__PURE__ */ new Set());
3138
+ }
3139
+ }
3140
+ }, [updateVisuals, drag, onButtonUp]);
3141
+ useTouchEvents(dpadRef, {
3142
+ onTouchStart: handleTouchStart,
3143
+ onTouchMove: handleTouchMove,
3144
+ onTouchEnd: handleTouchEnd,
3145
+ onTouchCancel: handleTouchEnd
3146
+ }, { cleanup: drag.clearDragTimer });
3147
+ const leftPx = displayX / 100 * containerWidth - size / 2;
3148
+ const topPx = displayY / 100 * containerHeight - size / 2;
3149
+ const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
3150
+ const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
3151
+ const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
3152
+ const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
3153
+ return /* @__PURE__ */ jsxs(
3154
+ "div",
3155
+ {
3156
+ ref: dpadRef,
3157
+ className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3158
+ style: {
3159
+ top: 0,
3160
+ left: 0,
3161
+ transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3162
+ width: size,
3163
+ height: size,
3164
+ opacity: isLandscape ? 0.75 : 0.9,
3165
+ WebkitTouchCallout: "none",
3166
+ WebkitUserSelect: "none",
3167
+ touchAction: "none",
3168
+ transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3169
+ },
3170
+ children: [
3171
+ /* @__PURE__ */ 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"}` }),
3172
+ /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3173
+ /* @__PURE__ */ 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" }),
3174
+ /* @__PURE__ */ 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" }),
3175
+ /* @__PURE__ */ 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" }),
3176
+ /* @__PURE__ */ 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" }),
3177
+ /* @__PURE__ */ jsx(
3178
+ "circle",
3179
+ {
3180
+ ref: centerCircleRef,
3181
+ cx: "50",
3182
+ cy: "50",
3183
+ r: "12",
3184
+ fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3185
+ stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3186
+ strokeWidth: drag.isDragging ? 2 : 1
3187
+ }
3188
+ ),
3189
+ /* @__PURE__ */ 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" }),
3190
+ /* @__PURE__ */ 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" }),
3191
+ /* @__PURE__ */ 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" }),
3192
+ /* @__PURE__ */ 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" })
3193
+ ] })
3194
+ ]
3195
+ }
3196
+ );
3197
+ });
3198
+ var Dpad_default = Dpad;
2952
3199
 
2953
- // src/lib/controls/types.ts
2954
- var DPAD_BUTTONS2 = ["up", "down", "left", "right"];
2955
- var FACE_BUTTONS = ["a", "b", "x", "y"];
2956
- var SHOULDER_BUTTONS = ["l", "r"];
2957
- var TRIGGER_BUTTONS = ["l2", "r2"];
2958
- var STICK_BUTTONS = ["l3", "r3"];
2959
- var SYSTEM_BUTTONS = ["start", "select"];
2960
- var ALL_BUTTONS = [
2961
- ...DPAD_BUTTONS2,
2962
- ...FACE_BUTTONS,
2963
- ...SHOULDER_BUTTONS,
2964
- ...TRIGGER_BUTTONS,
2965
- ...STICK_BUTTONS,
2966
- ...SYSTEM_BUTTONS
2967
- ];
2968
-
2969
- // src/lib/controls/defaults.ts
2970
- var STANDARD_GAMEPAD_BUTTONS = {
2971
- // Face buttons (Xbox/PlayStation layout)
2972
- a: 0,
2973
- // A / Cross (bottom)
2974
- b: 1,
2975
- // B / Circle (right)
2976
- x: 2,
2977
- // X / Square (left)
2978
- y: 3,
2979
- // Y / Triangle (top)
2980
- // Shoulders
2981
- l: 4,
2982
- // LB / L1
2983
- r: 5,
2984
- // RB / R1
2985
- // Triggers
2986
- l2: 6,
2987
- // LT / L2
2988
- r2: 7,
3200
+ // src/components/VirtualController/positioning.ts
3201
+ function adjustButtonPosition(config, context) {
3202
+ const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
3203
+ return {
3204
+ ...config,
3205
+ size: Math.floor(config.size * sizeMultiplier)
3206
+ };
3207
+ }
3208
+ var STORAGE_KEY = "virtual-button-positions";
3209
+ function useButtonPositions() {
3210
+ const [landscapePositions, setLandscapePositions] = useState({});
3211
+ const [portraitPositions, setPortraitPositions] = useState({});
3212
+ useEffect(() => {
3213
+ try {
3214
+ const stored = localStorage.getItem(STORAGE_KEY);
3215
+ if (stored) {
3216
+ const parsed = JSON.parse(stored);
3217
+ if (parsed.landscape && parsed.portrait) {
3218
+ setLandscapePositions(parsed.landscape);
3219
+ setPortraitPositions(parsed.portrait);
3220
+ } else {
3221
+ setLandscapePositions(parsed);
3222
+ }
3223
+ }
3224
+ } catch (e) {
3225
+ console.error("Failed to load button positions:", e);
3226
+ }
3227
+ }, []);
3228
+ const savePosition = useCallback((buttonType, x, y, isLandscape) => {
3229
+ if (isLandscape) {
3230
+ setLandscapePositions((prev) => {
3231
+ const updated = { ...prev, [buttonType]: { x, y } };
3232
+ try {
3233
+ const stored = {
3234
+ landscape: updated,
3235
+ portrait: portraitPositions
3236
+ };
3237
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3238
+ } catch (e) {
3239
+ console.error("Failed to save button position:", e);
3240
+ }
3241
+ return updated;
3242
+ });
3243
+ } else {
3244
+ setPortraitPositions((prev) => {
3245
+ const updated = { ...prev, [buttonType]: { x, y } };
3246
+ try {
3247
+ const stored = {
3248
+ landscape: landscapePositions,
3249
+ portrait: updated
3250
+ };
3251
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3252
+ } catch (e) {
3253
+ console.error("Failed to save button position:", e);
3254
+ }
3255
+ return updated;
3256
+ });
3257
+ }
3258
+ }, [landscapePositions, portraitPositions]);
3259
+ const getPosition = useCallback((buttonType, isLandscape) => {
3260
+ const positions = isLandscape ? landscapePositions : portraitPositions;
3261
+ return positions[buttonType] || null;
3262
+ }, [landscapePositions, portraitPositions]);
3263
+ const resetPositions = useCallback(() => {
3264
+ setLandscapePositions({});
3265
+ setPortraitPositions({});
3266
+ try {
3267
+ localStorage.removeItem(STORAGE_KEY);
3268
+ } catch (e) {
3269
+ console.error("Failed to reset button positions:", e);
3270
+ }
3271
+ }, []);
3272
+ return {
3273
+ landscapePositions,
3274
+ portraitPositions,
3275
+ savePosition,
3276
+ getPosition,
3277
+ resetPositions
3278
+ };
3279
+ }
3280
+
3281
+ // src/lib/controls/types.ts
3282
+ var DPAD_BUTTONS2 = ["up", "down", "left", "right"];
3283
+ var FACE_BUTTONS = ["a", "b", "x", "y"];
3284
+ var SHOULDER_BUTTONS = ["l", "r"];
3285
+ var TRIGGER_BUTTONS = ["l2", "r2"];
3286
+ var STICK_BUTTONS = ["l3", "r3"];
3287
+ var SYSTEM_BUTTONS = ["start", "select"];
3288
+ var ALL_BUTTONS = [
3289
+ ...DPAD_BUTTONS2,
3290
+ ...FACE_BUTTONS,
3291
+ ...SHOULDER_BUTTONS,
3292
+ ...TRIGGER_BUTTONS,
3293
+ ...STICK_BUTTONS,
3294
+ ...SYSTEM_BUTTONS
3295
+ ];
3296
+
3297
+ // src/lib/controls/defaults.ts
3298
+ var STANDARD_GAMEPAD_BUTTONS = {
3299
+ // Face buttons (Xbox/PlayStation layout)
3300
+ a: 0,
3301
+ // A / Cross (bottom)
3302
+ b: 1,
3303
+ // B / Circle (right)
3304
+ x: 2,
3305
+ // X / Square (left)
3306
+ y: 3,
3307
+ // Y / Triangle (top)
3308
+ // Shoulders
3309
+ l: 4,
3310
+ // LB / L1
3311
+ r: 5,
3312
+ // RB / R1
3313
+ // Triggers
3314
+ l2: 6,
3315
+ // LT / L2
3316
+ r2: 7,
2989
3317
  // RT / R2
2990
3318
  // System
2991
3319
  select: 8,
@@ -3535,351 +3863,6 @@ function getKeyboardCode(buttonType, controls) {
3535
3863
  }
3536
3864
  return DEFAULT_CONTROLS[key] ?? null;
3537
3865
  }
3538
- function getKeyName(code) {
3539
- if (code.startsWith("Key")) return code.slice(3).toLowerCase();
3540
- if (code.startsWith("Arrow")) return code.slice(5);
3541
- if (code === "Enter") return "Enter";
3542
- if (code === "ShiftRight" || code === "ShiftLeft") return "Shift";
3543
- return code;
3544
- }
3545
- function getCanvas() {
3546
- return document.querySelector(".game-canvas-container canvas") || document.querySelector("canvas");
3547
- }
3548
- function dispatchKeyboardEvent(type, code) {
3549
- const canvas = getCanvas();
3550
- if (!canvas) {
3551
- return false;
3552
- }
3553
- const event = new KeyboardEvent(type, {
3554
- code,
3555
- key: getKeyName(code),
3556
- bubbles: true,
3557
- cancelable: true
3558
- });
3559
- canvas.dispatchEvent(event);
3560
- return true;
3561
- }
3562
- var CENTER_TOUCH_RADIUS = 0.35;
3563
- var Dpad = React2.memo(function Dpad2({
3564
- size = 180,
3565
- x,
3566
- y,
3567
- containerWidth,
3568
- containerHeight,
3569
- controls,
3570
- systemColor = "#00FF41",
3571
- isLandscape = false,
3572
- customPosition,
3573
- onPositionChange
3574
- }) {
3575
- const dpadRef = useRef(null);
3576
- const activeTouchRef = useRef(null);
3577
- const activeDirectionsRef = useRef(/* @__PURE__ */ new Set());
3578
- const upPathRef = useRef(null);
3579
- const downPathRef = useRef(null);
3580
- const leftPathRef = useRef(null);
3581
- const rightPathRef = useRef(null);
3582
- const centerCircleRef = useRef(null);
3583
- const displayX = customPosition ? customPosition.x : x;
3584
- const displayY = customPosition ? customPosition.y : y;
3585
- const releaseAllDirections = useCallback((getKeyCode2) => {
3586
- activeDirectionsRef.current.forEach((dir) => {
3587
- const keyCode = getKeyCode2(dir);
3588
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3589
- });
3590
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3591
- }, []);
3592
- const getKeyCode = useCallback((direction) => {
3593
- if (!controls) {
3594
- const defaults = {
3595
- up: "ArrowUp",
3596
- down: "ArrowDown",
3597
- left: "ArrowLeft",
3598
- right: "ArrowRight"
3599
- };
3600
- return defaults[direction];
3601
- }
3602
- return controls[direction] || "";
3603
- }, [controls]);
3604
- const drag = useDrag({
3605
- elementSize: size,
3606
- displayX,
3607
- displayY,
3608
- containerWidth,
3609
- containerHeight,
3610
- onPositionChange,
3611
- centerThreshold: CENTER_TOUCH_RADIUS,
3612
- onDragStart: () => {
3613
- releaseAllDirections(getKeyCode);
3614
- updateVisuals(/* @__PURE__ */ new Set());
3615
- }
3616
- });
3617
- const getDirectionsFromTouch = useCallback((touchX, touchY, rect) => {
3618
- const centerX = rect.left + rect.width / 2;
3619
- const centerY = rect.top + rect.height / 2;
3620
- const dx = touchX - centerX;
3621
- const dy = touchY - centerY;
3622
- const distance = Math.sqrt(dx * dx + dy * dy);
3623
- const deadZone = rect.width / 2 * 0.15;
3624
- if (distance < deadZone) return /* @__PURE__ */ new Set();
3625
- const directions = /* @__PURE__ */ new Set();
3626
- const angle = Math.atan2(dy, dx) * (180 / Math.PI);
3627
- if (angle >= -150 && angle <= -30) directions.add("up");
3628
- if (angle >= 30 && angle <= 150) directions.add("down");
3629
- if (angle >= 120 || angle <= -120) directions.add("left");
3630
- if (angle >= -60 && angle <= 60) directions.add("right");
3631
- return directions;
3632
- }, []);
3633
- const updateVisuals = useCallback((directions) => {
3634
- const activeFill = `${systemColor}80`;
3635
- const inactiveFill = "rgba(255, 255, 255, 0.05)";
3636
- const activeStroke = systemColor;
3637
- const inactiveStroke = "rgba(255, 255, 255, 0.2)";
3638
- const glow = `0 0 15px ${systemColor}`;
3639
- const updatePart = (ref, isActive) => {
3640
- if (ref.current) {
3641
- ref.current.style.fill = isActive ? activeFill : inactiveFill;
3642
- ref.current.style.stroke = isActive ? activeStroke : inactiveStroke;
3643
- ref.current.style.filter = isActive ? `drop-shadow(${glow})` : "none";
3644
- ref.current.style.transform = isActive ? "scale(0.98)" : "scale(1)";
3645
- ref.current.style.transformOrigin = "center";
3646
- }
3647
- };
3648
- updatePart(upPathRef, directions.has("up"));
3649
- updatePart(downPathRef, directions.has("down"));
3650
- updatePart(leftPathRef, directions.has("left"));
3651
- updatePart(rightPathRef, directions.has("right"));
3652
- if (centerCircleRef.current) {
3653
- const isAny = directions.size > 0;
3654
- centerCircleRef.current.style.fill = isAny ? systemColor : "rgba(0,0,0,0.5)";
3655
- centerCircleRef.current.style.stroke = isAny ? "#fff" : "rgba(255,255,255,0.3)";
3656
- }
3657
- }, [systemColor]);
3658
- const updateDirections = useCallback((newDirections) => {
3659
- const prev = activeDirectionsRef.current;
3660
- prev.forEach((dir) => {
3661
- if (!newDirections.has(dir)) {
3662
- const keyCode = getKeyCode(dir);
3663
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3664
- }
3665
- });
3666
- newDirections.forEach((dir) => {
3667
- if (!prev.has(dir)) {
3668
- const keyCode = getKeyCode(dir);
3669
- if (keyCode) {
3670
- if (navigator.vibrate) navigator.vibrate(5);
3671
- dispatchKeyboardEvent("keydown", keyCode);
3672
- }
3673
- }
3674
- });
3675
- activeDirectionsRef.current = newDirections;
3676
- updateVisuals(newDirections);
3677
- }, [getKeyCode, updateVisuals]);
3678
- const handleTouchStart = useCallback((e) => {
3679
- e.preventDefault();
3680
- if (activeTouchRef.current !== null) return;
3681
- const touch = e.changedTouches[0];
3682
- activeTouchRef.current = touch.identifier;
3683
- const rect = dpadRef.current?.getBoundingClientRect();
3684
- if (!rect) return;
3685
- if (onPositionChange) {
3686
- drag.checkDragStart(touch.clientX, touch.clientY, rect);
3687
- }
3688
- if (!drag.isDragging) {
3689
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3690
- }
3691
- }, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
3692
- const handleTouchMove = useCallback((e) => {
3693
- e.preventDefault();
3694
- let touch = null;
3695
- for (let i = 0; i < e.changedTouches.length; i++) {
3696
- if (e.changedTouches[i].identifier === activeTouchRef.current) {
3697
- touch = e.changedTouches[i];
3698
- break;
3699
- }
3700
- }
3701
- if (!touch) return;
3702
- if (drag.isDragging) {
3703
- drag.handleDragMove(touch.clientX, touch.clientY);
3704
- } else if (onPositionChange) {
3705
- const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
3706
- if (!startedDrag) {
3707
- drag.clearDragTimer();
3708
- const rect = dpadRef.current?.getBoundingClientRect();
3709
- if (rect) {
3710
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3711
- }
3712
- }
3713
- } else {
3714
- const rect = dpadRef.current?.getBoundingClientRect();
3715
- if (rect) {
3716
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3717
- }
3718
- }
3719
- }, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
3720
- const handleTouchEnd = useCallback((e) => {
3721
- e.preventDefault();
3722
- drag.clearDragTimer();
3723
- let touchEnded = false;
3724
- for (let i = 0; i < e.changedTouches.length; i++) {
3725
- if (e.changedTouches[i].identifier === activeTouchRef.current) {
3726
- touchEnded = true;
3727
- break;
3728
- }
3729
- }
3730
- if (touchEnded) {
3731
- activeTouchRef.current = null;
3732
- if (drag.isDragging) {
3733
- drag.handleDragEnd();
3734
- } else {
3735
- activeDirectionsRef.current.forEach((dir) => {
3736
- const keyCode = getKeyCode(dir);
3737
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3738
- });
3739
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3740
- updateVisuals(/* @__PURE__ */ new Set());
3741
- }
3742
- }
3743
- }, [getKeyCode, updateVisuals, drag]);
3744
- useTouchEvents(dpadRef, {
3745
- onTouchStart: handleTouchStart,
3746
- onTouchMove: handleTouchMove,
3747
- onTouchEnd: handleTouchEnd,
3748
- onTouchCancel: handleTouchEnd
3749
- }, { cleanup: drag.clearDragTimer });
3750
- const leftPx = displayX / 100 * containerWidth - size / 2;
3751
- const topPx = displayY / 100 * containerHeight - size / 2;
3752
- const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
3753
- const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
3754
- const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
3755
- const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
3756
- return /* @__PURE__ */ jsxs(
3757
- "div",
3758
- {
3759
- ref: dpadRef,
3760
- className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3761
- style: {
3762
- top: 0,
3763
- left: 0,
3764
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3765
- width: size,
3766
- height: size,
3767
- opacity: isLandscape ? 0.75 : 0.9,
3768
- WebkitTouchCallout: "none",
3769
- WebkitUserSelect: "none",
3770
- touchAction: "none",
3771
- transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3772
- },
3773
- children: [
3774
- /* @__PURE__ */ 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"}` }),
3775
- /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3776
- /* @__PURE__ */ 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" }),
3777
- /* @__PURE__ */ 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" }),
3778
- /* @__PURE__ */ 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" }),
3779
- /* @__PURE__ */ 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" }),
3780
- /* @__PURE__ */ jsx(
3781
- "circle",
3782
- {
3783
- ref: centerCircleRef,
3784
- cx: "50",
3785
- cy: "50",
3786
- r: "12",
3787
- fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3788
- stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3789
- strokeWidth: drag.isDragging ? 2 : 1
3790
- }
3791
- ),
3792
- /* @__PURE__ */ 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" }),
3793
- /* @__PURE__ */ 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" }),
3794
- /* @__PURE__ */ 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" }),
3795
- /* @__PURE__ */ 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" })
3796
- ] })
3797
- ]
3798
- }
3799
- );
3800
- });
3801
- var Dpad_default = Dpad;
3802
-
3803
- // src/components/VirtualController/positioning.ts
3804
- function adjustButtonPosition(config, context) {
3805
- const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
3806
- return {
3807
- ...config,
3808
- size: Math.floor(config.size * sizeMultiplier)
3809
- };
3810
- }
3811
- var STORAGE_KEY = "virtual-button-positions";
3812
- function useButtonPositions() {
3813
- const [landscapePositions, setLandscapePositions] = useState({});
3814
- const [portraitPositions, setPortraitPositions] = useState({});
3815
- useEffect(() => {
3816
- try {
3817
- const stored = localStorage.getItem(STORAGE_KEY);
3818
- if (stored) {
3819
- const parsed = JSON.parse(stored);
3820
- if (parsed.landscape && parsed.portrait) {
3821
- setLandscapePositions(parsed.landscape);
3822
- setPortraitPositions(parsed.portrait);
3823
- } else {
3824
- setLandscapePositions(parsed);
3825
- }
3826
- }
3827
- } catch (e) {
3828
- console.error("Failed to load button positions:", e);
3829
- }
3830
- }, []);
3831
- const savePosition = useCallback((buttonType, x, y, isLandscape) => {
3832
- if (isLandscape) {
3833
- setLandscapePositions((prev) => {
3834
- const updated = { ...prev, [buttonType]: { x, y } };
3835
- try {
3836
- const stored = {
3837
- landscape: updated,
3838
- portrait: portraitPositions
3839
- };
3840
- localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3841
- } catch (e) {
3842
- console.error("Failed to save button position:", e);
3843
- }
3844
- return updated;
3845
- });
3846
- } else {
3847
- setPortraitPositions((prev) => {
3848
- const updated = { ...prev, [buttonType]: { x, y } };
3849
- try {
3850
- const stored = {
3851
- landscape: landscapePositions,
3852
- portrait: updated
3853
- };
3854
- localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3855
- } catch (e) {
3856
- console.error("Failed to save button position:", e);
3857
- }
3858
- return updated;
3859
- });
3860
- }
3861
- }, [landscapePositions, portraitPositions]);
3862
- const getPosition = useCallback((buttonType, isLandscape) => {
3863
- const positions = isLandscape ? landscapePositions : portraitPositions;
3864
- return positions[buttonType] || null;
3865
- }, [landscapePositions, portraitPositions]);
3866
- const resetPositions = useCallback(() => {
3867
- setLandscapePositions({});
3868
- setPortraitPositions({});
3869
- try {
3870
- localStorage.removeItem(STORAGE_KEY);
3871
- } catch (e) {
3872
- console.error("Failed to reset button positions:", e);
3873
- }
3874
- }, []);
3875
- return {
3876
- landscapePositions,
3877
- portraitPositions,
3878
- savePosition,
3879
- getPosition,
3880
- resetPositions
3881
- };
3882
- }
3883
3866
  var STORAGE_KEY2 = "koin-controls-hint-shown";
3884
3867
  function ControlsHint({ isVisible }) {
3885
3868
  const [show, setShow] = useState(false);
@@ -3964,7 +3947,7 @@ function LockButton({
3964
3947
  "button",
3965
3948
  {
3966
3949
  onClick: onToggle,
3967
- 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",
3950
+ className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
3968
3951
  style: {
3969
3952
  backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3970
3953
  border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
@@ -3978,18 +3961,164 @@ function LockButton({
3978
3961
  }
3979
3962
  )
3980
3963
  }
3981
- );
3964
+ );
3965
+ }
3966
+ function HoldButton({
3967
+ isActive,
3968
+ onToggle,
3969
+ systemColor = "#00FF41"
3970
+ }) {
3971
+ const Icon = isActive ? StopCircle : Hand;
3972
+ return /* @__PURE__ */ jsx(
3973
+ "button",
3974
+ {
3975
+ onClick: onToggle,
3976
+ className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
3977
+ style: {
3978
+ backgroundColor: isActive ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3979
+ border: `1px solid ${isActive ? "rgba(255,255,255,0.4)" : systemColor}`
3980
+ },
3981
+ "aria-label": isActive ? "Disable Hold Mode" : "Enable Button Hold Mode",
3982
+ children: /* @__PURE__ */ jsx(
3983
+ Icon,
3984
+ {
3985
+ size: 18,
3986
+ style: { color: isActive ? "#fff" : systemColor }
3987
+ }
3988
+ )
3989
+ }
3990
+ );
3991
+ }
3992
+ function TurboButton({
3993
+ isActive,
3994
+ onToggle,
3995
+ systemColor = "#00FF41"
3996
+ }) {
3997
+ const Icon = isActive ? ZapOff : Zap;
3998
+ return /* @__PURE__ */ jsx(
3999
+ "button",
4000
+ {
4001
+ onClick: onToggle,
4002
+ className: "pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
4003
+ style: {
4004
+ backgroundColor: isActive ? "rgba(255,200,0,0.3)" : `${systemColor}20`,
4005
+ border: `1px solid ${isActive ? "rgba(255,200,0,0.6)" : systemColor}`
4006
+ },
4007
+ "aria-label": isActive ? "Disable Turbo Mode" : "Enable Turbo Fire Mode",
4008
+ children: /* @__PURE__ */ jsx(
4009
+ Icon,
4010
+ {
4011
+ size: 18,
4012
+ style: { color: isActive ? "#FFC800" : systemColor }
4013
+ }
4014
+ )
4015
+ }
4016
+ );
4017
+ }
4018
+ var MODE_CONFIG = {
4019
+ hold: {
4020
+ Icon: Hand,
4021
+ title: "Hold Mode",
4022
+ instruction: "Tap a button to hold it",
4023
+ buttonIcon: Hand,
4024
+ buttonColor: "#22c55e"
4025
+ // green
4026
+ },
4027
+ turbo: {
4028
+ Icon: Zap,
4029
+ title: "Turbo Mode",
4030
+ instruction: "Tap a button for rapid fire",
4031
+ buttonIcon: Zap,
4032
+ buttonColor: "#fbbf24"
4033
+ // yellow
4034
+ }
4035
+ };
4036
+ function ModeOverlay({
4037
+ mode,
4038
+ heldButtons,
4039
+ turboButtons,
4040
+ systemColor = "#00FF41",
4041
+ onExit
4042
+ }) {
4043
+ if (!mode) return null;
4044
+ const config = MODE_CONFIG[mode];
4045
+ const buttons = mode === "hold" ? heldButtons : turboButtons;
4046
+ const buttonArray = Array.from(buttons);
4047
+ const { Icon, buttonIcon: ButtonIcon } = config;
4048
+ return /* @__PURE__ */ jsx("div", { className: "fixed top-0 left-0 right-0 z-40 flex justify-center pt-4 pointer-events-none", children: /* @__PURE__ */ jsxs(
4049
+ "div",
4050
+ {
4051
+ className: "relative px-5 py-3 rounded-2xl backdrop-blur-md border pointer-events-auto",
4052
+ style: {
4053
+ backgroundColor: "rgba(0,0,0,0.85)",
4054
+ borderColor: `${systemColor}60`,
4055
+ boxShadow: `0 4px 20px ${systemColor}30`
4056
+ },
4057
+ children: [
4058
+ /* @__PURE__ */ jsx(
4059
+ "button",
4060
+ {
4061
+ onClick: onExit,
4062
+ className: "absolute -top-2 -right-2 w-7 h-7 rounded-full flex items-center justify-center transition-transform active:scale-90",
4063
+ style: {
4064
+ backgroundColor: "#ef4444",
4065
+ border: "2px solid rgba(255,255,255,0.3)"
4066
+ },
4067
+ "aria-label": "Exit mode",
4068
+ children: /* @__PURE__ */ jsx(X, { size: 14, color: "white", strokeWidth: 3 })
4069
+ }
4070
+ ),
4071
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4072
+ /* @__PURE__ */ jsx(
4073
+ "div",
4074
+ {
4075
+ className: "w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0",
4076
+ style: { backgroundColor: `${systemColor}30` },
4077
+ children: /* @__PURE__ */ jsx(Icon, { size: 20, style: { color: systemColor } })
4078
+ }
4079
+ ),
4080
+ /* @__PURE__ */ jsxs("div", { className: "text-left", children: [
4081
+ /* @__PURE__ */ jsx("h3", { className: "text-white font-bold text-sm", children: config.title }),
4082
+ /* @__PURE__ */ jsx("p", { className: "text-white/60 text-xs", children: config.instruction })
4083
+ ] })
4084
+ ] }),
4085
+ buttonArray.length > 0 && /* @__PURE__ */ 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__ */ jsxs(
4086
+ "span",
4087
+ {
4088
+ className: "px-2 py-0.5 rounded-full text-[10px] font-bold uppercase flex items-center gap-1",
4089
+ style: {
4090
+ backgroundColor: `${config.buttonColor}25`,
4091
+ color: config.buttonColor
4092
+ },
4093
+ children: [
4094
+ /* @__PURE__ */ jsx(ButtonIcon, { size: 10 }),
4095
+ button
4096
+ ]
4097
+ },
4098
+ button
4099
+ )) })
4100
+ ]
4101
+ }
4102
+ ) });
3982
4103
  }
3983
4104
  var LOCK_KEY = "koin-controls-locked";
3984
4105
  function VirtualController({
3985
4106
  system,
3986
4107
  isRunning,
3987
4108
  controls,
3988
- systemColor = "#00FF41"
4109
+ systemColor = "#00FF41",
3989
4110
  // Default retro green
4111
+ hapticsEnabled = true,
4112
+ onButtonDown,
4113
+ onButtonUp,
4114
+ onPause,
4115
+ onResume
3990
4116
  }) {
3991
4117
  const { isMobile, isLandscape, isPortrait } = useMobile();
3992
4118
  const [pressedButtons, setPressedButtons] = useState(/* @__PURE__ */ new Set());
4119
+ const pressedButtonsRef = useRef(/* @__PURE__ */ new Set());
4120
+ const [heldButtons, setHeldButtons] = useState(/* @__PURE__ */ new Set());
4121
+ const [isHoldMode, setIsHoldMode] = useState(false);
3993
4122
  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
3994
4123
  const [isFullscreenState, setIsFullscreenState] = useState(false);
3995
4124
  const [isLocked, setIsLocked] = useState(true);
@@ -4007,6 +4136,28 @@ function VirtualController({
4007
4136
  return newValue;
4008
4137
  });
4009
4138
  }, []);
4139
+ const toggleHoldMode = useCallback(() => {
4140
+ setIsHoldMode((prev) => {
4141
+ if (prev) {
4142
+ onResume();
4143
+ } else {
4144
+ onPause();
4145
+ }
4146
+ return !prev;
4147
+ });
4148
+ }, [onPause, onResume]);
4149
+ const [isTurboMode, setIsTurboMode] = useState(false);
4150
+ const [turboButtons, setTurboButtons] = useState(/* @__PURE__ */ new Set());
4151
+ const toggleTurboMode = useCallback(() => {
4152
+ setIsTurboMode((prev) => {
4153
+ if (prev) {
4154
+ onResume();
4155
+ } else {
4156
+ onPause();
4157
+ }
4158
+ return !prev;
4159
+ });
4160
+ }, [onPause, onResume]);
4010
4161
  const layout = getLayoutForSystem(system);
4011
4162
  const visibleButtons = layout.buttons.filter((btn) => {
4012
4163
  if (isPortrait) {
@@ -4076,9 +4227,9 @@ function VirtualController({
4076
4227
  return;
4077
4228
  }
4078
4229
  setPressedButtons((prev) => new Set(prev).add(buttonType));
4079
- dispatchKeyboardEvent("keydown", keyboardCode);
4230
+ onButtonDown(buttonType);
4080
4231
  setTimeout(() => {
4081
- dispatchKeyboardEvent("keyup", keyboardCode);
4232
+ onButtonUp(buttonType);
4082
4233
  setPressedButtons((prev) => {
4083
4234
  const next = new Set(prev);
4084
4235
  next.delete(buttonType);
@@ -4086,7 +4237,7 @@ function VirtualController({
4086
4237
  });
4087
4238
  }, 100);
4088
4239
  },
4089
- [isRunning, getButtonKeyboardCode]
4240
+ [isRunning, getButtonKeyboardCode, onButtonDown, onButtonUp]
4090
4241
  );
4091
4242
  const handlePressDown = useCallback(
4092
4243
  (buttonType) => {
@@ -4095,15 +4246,70 @@ function VirtualController({
4095
4246
  if (isSystemButton) return;
4096
4247
  const keyboardCode = getButtonKeyboardCode(buttonType);
4097
4248
  if (!keyboardCode) return;
4249
+ if (isHoldMode) {
4250
+ const isHeld = heldButtons.has(buttonType);
4251
+ if (isHeld) {
4252
+ setHeldButtons((prev) => {
4253
+ const next = new Set(prev);
4254
+ next.delete(buttonType);
4255
+ return next;
4256
+ });
4257
+ onButtonUp(buttonType);
4258
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
4259
+ } else {
4260
+ setHeldButtons((prev) => {
4261
+ setTurboButtons((turboPrev) => {
4262
+ if (turboPrev.has(buttonType)) {
4263
+ const nextTurbo = new Set(turboPrev);
4264
+ nextTurbo.delete(buttonType);
4265
+ return nextTurbo;
4266
+ }
4267
+ return turboPrev;
4268
+ });
4269
+ return new Set(prev).add(buttonType);
4270
+ });
4271
+ onButtonDown(buttonType);
4272
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate([10, 30, 10]);
4273
+ }
4274
+ return;
4275
+ }
4276
+ if (isTurboMode) {
4277
+ const isTurbo = turboButtons.has(buttonType);
4278
+ if (isTurbo) {
4279
+ setTurboButtons((prev) => {
4280
+ const next = new Set(prev);
4281
+ next.delete(buttonType);
4282
+ return next;
4283
+ });
4284
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
4285
+ } else {
4286
+ setTurboButtons((prev) => {
4287
+ setHeldButtons((holdPrev) => {
4288
+ if (holdPrev.has(buttonType)) {
4289
+ const nextHold = new Set(holdPrev);
4290
+ nextHold.delete(buttonType);
4291
+ onButtonUp(buttonType);
4292
+ return nextHold;
4293
+ }
4294
+ return holdPrev;
4295
+ });
4296
+ return new Set(prev).add(buttonType);
4297
+ });
4298
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate([5, 10, 5]);
4299
+ }
4300
+ return;
4301
+ }
4098
4302
  setPressedButtons((prev) => {
4099
4303
  if (prev.has(buttonType)) return prev;
4100
4304
  const next = new Set(prev);
4101
4305
  next.add(buttonType);
4102
4306
  return next;
4103
4307
  });
4104
- dispatchKeyboardEvent("keydown", keyboardCode);
4308
+ if (!heldButtons.has(buttonType)) {
4309
+ onButtonDown(buttonType);
4310
+ }
4105
4311
  },
4106
- [isRunning, getButtonKeyboardCode]
4312
+ [isRunning, getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonDown, onButtonUp]
4107
4313
  );
4108
4314
  const handleRelease = useCallback(
4109
4315
  (buttonType) => {
@@ -4111,15 +4317,29 @@ function VirtualController({
4111
4317
  if (isSystemButton) return;
4112
4318
  const keyboardCode = getButtonKeyboardCode(buttonType);
4113
4319
  if (!keyboardCode) return;
4320
+ if (isHoldMode) return;
4321
+ if (heldButtons.has(buttonType)) {
4322
+ if (!isHoldMode) {
4323
+ setHeldButtons((prev) => {
4324
+ const next = new Set(prev);
4325
+ next.delete(buttonType);
4326
+ return next;
4327
+ });
4328
+ onButtonUp(buttonType);
4329
+ if (hapticsEnabled && navigator.vibrate) navigator.vibrate(10);
4330
+ }
4331
+ return;
4332
+ }
4333
+ if (isTurboMode) return;
4114
4334
  setPressedButtons((prev) => {
4115
4335
  if (!prev.has(buttonType)) return prev;
4116
4336
  const next = new Set(prev);
4117
4337
  next.delete(buttonType);
4118
4338
  return next;
4119
4339
  });
4120
- dispatchKeyboardEvent("keyup", keyboardCode);
4340
+ onButtonUp(buttonType);
4121
4341
  },
4122
- [getButtonKeyboardCode]
4342
+ [getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonUp]
4123
4343
  );
4124
4344
  useEffect(() => {
4125
4345
  if (!isRunning && pressedButtons.size > 0) {
@@ -4131,6 +4351,22 @@ function VirtualController({
4131
4351
  setPressedButtons(/* @__PURE__ */ new Set());
4132
4352
  }
4133
4353
  }, [isRunning, pressedButtons, handleRelease]);
4354
+ const TURBO_RATE = 15;
4355
+ useEffect(() => {
4356
+ pressedButtonsRef.current = pressedButtons;
4357
+ }, [pressedButtons]);
4358
+ useEffect(() => {
4359
+ if (isTurboMode || turboButtons.size === 0) return;
4360
+ const interval = setInterval(() => {
4361
+ turboButtons.forEach((buttonType) => {
4362
+ if (pressedButtonsRef.current.has(buttonType)) {
4363
+ onButtonDown(buttonType);
4364
+ setTimeout(() => onButtonUp(buttonType), 25);
4365
+ }
4366
+ });
4367
+ }, 1e3 / TURBO_RATE);
4368
+ return () => clearInterval(interval);
4369
+ }, [isTurboMode, turboButtons, onButtonDown, onButtonUp]);
4134
4370
  const memoizedButtonElements = useMemo(() => {
4135
4371
  const width = containerSize.width || (typeof window !== "undefined" ? window.innerWidth : 0);
4136
4372
  const height = containerSize.height || (typeof window !== "undefined" ? window.innerHeight : 0);
@@ -4161,14 +4397,32 @@ function VirtualController({
4161
4397
  className: "fixed inset-0 z-30 pointer-events-none",
4162
4398
  style: { touchAction: "none" },
4163
4399
  children: [
4164
- /* @__PURE__ */ jsx(
4165
- LockButton,
4166
- {
4167
- isLocked,
4168
- onToggle: toggleLock,
4169
- systemColor
4170
- }
4171
- ),
4400
+ /* @__PURE__ */ jsxs("div", { className: "fixed top-4 left-1/2 -translate-x-1/2 z-40 flex gap-4", children: [
4401
+ /* @__PURE__ */ jsx(
4402
+ LockButton,
4403
+ {
4404
+ isLocked,
4405
+ onToggle: toggleLock,
4406
+ systemColor
4407
+ }
4408
+ ),
4409
+ /* @__PURE__ */ jsx(
4410
+ HoldButton,
4411
+ {
4412
+ isActive: isHoldMode,
4413
+ onToggle: toggleHoldMode,
4414
+ systemColor
4415
+ }
4416
+ ),
4417
+ /* @__PURE__ */ jsx(
4418
+ TurboButton,
4419
+ {
4420
+ isActive: isTurboMode,
4421
+ onToggle: toggleTurboMode,
4422
+ systemColor
4423
+ }
4424
+ )
4425
+ ] }),
4172
4426
  /* @__PURE__ */ jsx(
4173
4427
  Dpad_default,
4174
4428
  {
@@ -4177,18 +4431,20 @@ function VirtualController({
4177
4431
  y: finalDpadY,
4178
4432
  containerWidth: containerSize.width || window.innerWidth,
4179
4433
  containerHeight: containerSize.height || window.innerHeight,
4180
- controls,
4181
4434
  systemColor,
4182
4435
  isLandscape,
4183
4436
  customPosition: getPosition("up", isLandscape),
4184
- onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
4437
+ onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape),
4438
+ hapticsEnabled,
4439
+ onButtonDown,
4440
+ onButtonUp
4185
4441
  }
4186
4442
  ),
4187
4443
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
4188
4444
  VirtualButton_default,
4189
4445
  {
4190
4446
  config: adjustedConfig,
4191
- isPressed: pressedButtons.has(buttonConfig.type),
4447
+ isPressed: pressedButtons.has(buttonConfig.type) || heldButtons.has(buttonConfig.type),
4192
4448
  onPress: handlePress,
4193
4449
  onPressDown: handlePressDown,
4194
4450
  onRelease: handleRelease,
@@ -4197,10 +4453,24 @@ function VirtualController({
4197
4453
  customPosition,
4198
4454
  onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4199
4455
  isLandscape,
4200
- console: layout.console
4456
+ console: layout.console,
4457
+ hapticsEnabled,
4458
+ mode: isHoldMode ? "hold" : isTurboMode ? "turbo" : "normal",
4459
+ isHeld: heldButtons.has(buttonConfig.type),
4460
+ isInTurbo: turboButtons.has(buttonConfig.type)
4201
4461
  },
4202
4462
  buttonConfig.type
4203
4463
  )),
4464
+ (isHoldMode || isTurboMode) && /* @__PURE__ */ jsx(
4465
+ ModeOverlay,
4466
+ {
4467
+ mode: isHoldMode ? "hold" : "turbo",
4468
+ heldButtons,
4469
+ turboButtons,
4470
+ systemColor,
4471
+ onExit: isHoldMode ? toggleHoldMode : toggleTurboMode
4472
+ }
4473
+ ),
4204
4474
  /* @__PURE__ */ jsx(ControlsHint, { isVisible: isRunning })
4205
4475
  ]
4206
4476
  }
@@ -4887,7 +5157,8 @@ function CheatModal({
4887
5157
  cheats,
4888
5158
  activeCheats,
4889
5159
  onToggle,
4890
- onClose
5160
+ onClose,
5161
+ onAddManualCheat
4891
5162
  }) {
4892
5163
  const t = useKoinTranslation();
4893
5164
  const [copiedId, setCopiedId] = React2.useState(null);
@@ -4905,54 +5176,111 @@ function CheatModal({
4905
5176
  subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4906
5177
  icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
4907
5178
  footer: /* @__PURE__ */ 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 }),
4908
- children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
4909
- /* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4910
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4911
- /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
4912
- ] }) : cheats.map((cheat) => {
4913
- const isActive = activeCheats.has(cheat.id);
4914
- return /* @__PURE__ */ jsxs(
4915
- "div",
4916
- {
4917
- className: `
4918
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4919
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4920
- `,
4921
- onClick: () => onToggle(cheat.id),
4922
- children: [
4923
- /* @__PURE__ */ jsx(
4924
- "div",
4925
- {
4926
- className: `
4927
- flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4928
- ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4929
- `,
4930
- children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
5179
+ children: /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-4 max-h-[400px] overflow-y-auto", children: [
5180
+ onAddManualCheat && /* @__PURE__ */ jsxs("div", { className: "p-3 bg-white/5 rounded-lg border border-white/10 space-y-3", children: [
5181
+ /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: /* @__PURE__ */ jsx(
5182
+ "input",
5183
+ {
5184
+ type: "text",
5185
+ placeholder: t.modals.cheats.codePlaceholder || "Enter cheat code",
5186
+ 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",
5187
+ onKeyDown: (e) => {
5188
+ if (e.key === "Enter") {
5189
+ const input = e.currentTarget;
5190
+ const code = input.value.trim();
5191
+ if (code) {
5192
+ const descInput = input.parentElement?.nextElementSibling?.querySelector("input");
5193
+ const desc = descInput?.value.trim() || "Custom Cheat";
5194
+ onAddManualCheat(code, desc);
5195
+ input.value = "";
5196
+ if (descInput) descInput.value = "";
5197
+ }
4931
5198
  }
4932
- ),
4933
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
4934
- /* @__PURE__ */ jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description }),
4935
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
4936
- /* @__PURE__ */ 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 }),
4937
- /* @__PURE__ */ jsx(
4938
- "button",
4939
- {
4940
- onClick: (e) => {
4941
- e.stopPropagation();
4942
- handleCopy(cheat.code, cheat.id);
4943
- },
4944
- className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
4945
- title: t.modals.cheats.copy,
4946
- children: copiedId === cheat.id ? /* @__PURE__ */ jsx(Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsx(Copy, { size: 14 })
4947
- }
4948
- )
5199
+ }
5200
+ }
5201
+ ) }),
5202
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
5203
+ /* @__PURE__ */ jsx(
5204
+ "input",
5205
+ {
5206
+ type: "text",
5207
+ placeholder: t.modals.cheats.descPlaceholder || "Description (optional)",
5208
+ 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"
5209
+ }
5210
+ ),
5211
+ /* @__PURE__ */ jsx(
5212
+ "button",
5213
+ {
5214
+ onClick: (e) => {
5215
+ const descInput = e.currentTarget.previousElementSibling;
5216
+ const codeInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input");
5217
+ const code = codeInput?.value.trim();
5218
+ const desc = descInput?.value.trim() || "Custom Cheat";
5219
+ if (code) {
5220
+ onAddManualCheat(code, desc);
5221
+ codeInput.value = "";
5222
+ descInput.value = "";
5223
+ }
5224
+ },
5225
+ className: "px-3 py-2 bg-purple-600 hover:bg-purple-500 text-white text-sm rounded transition-colors",
5226
+ children: t.modals.cheats.add || "Add"
5227
+ }
5228
+ )
5229
+ ] })
5230
+ ] }),
5231
+ cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
5232
+ /* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
5233
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
5234
+ /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
5235
+ ] }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: cheats.map((cheat) => {
5236
+ const isActive = activeCheats.has(cheat.id);
5237
+ const isManual = cheat.source === "manual";
5238
+ return /* @__PURE__ */ jsxs(
5239
+ "div",
5240
+ {
5241
+ className: `
5242
+ group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
5243
+ ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
5244
+ `,
5245
+ onClick: () => onToggle(cheat.id),
5246
+ children: [
5247
+ /* @__PURE__ */ jsx(
5248
+ "div",
5249
+ {
5250
+ className: `
5251
+ flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
5252
+ ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
5253
+ `,
5254
+ children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
5255
+ }
5256
+ ),
5257
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
5258
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
5259
+ isManual && /* @__PURE__ */ 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" }),
5260
+ /* @__PURE__ */ jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description })
5261
+ ] }),
5262
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
5263
+ /* @__PURE__ */ 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 }),
5264
+ /* @__PURE__ */ jsx(
5265
+ "button",
5266
+ {
5267
+ onClick: (e) => {
5268
+ e.stopPropagation();
5269
+ handleCopy(cheat.code, cheat.id);
5270
+ },
5271
+ className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
5272
+ title: t.modals.cheats.copy,
5273
+ children: copiedId === cheat.id ? /* @__PURE__ */ jsx(Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsx(Copy, { size: 14 })
5274
+ }
5275
+ )
5276
+ ] })
4949
5277
  ] })
4950
- ] })
4951
- ]
4952
- },
4953
- cheat.id
4954
- );
4955
- }) })
5278
+ ]
5279
+ },
5280
+ cheat.id
5281
+ );
5282
+ }) })
5283
+ ] })
4956
5284
  }
4957
5285
  );
4958
5286
  }
@@ -5284,7 +5612,9 @@ function SettingsModal({
5284
5612
  onClose,
5285
5613
  currentLanguage,
5286
5614
  onLanguageChange,
5287
- systemColor = "#00FF41"
5615
+ systemColor = "#00FF41",
5616
+ hapticsEnabled,
5617
+ onToggleHaptics
5288
5618
  }) {
5289
5619
  const t = useKoinTranslation();
5290
5620
  const languages = [
@@ -5308,30 +5638,57 @@ function SettingsModal({
5308
5638
  children: t.modals.shortcuts.pressEsc
5309
5639
  }
5310
5640
  ),
5311
- children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5312
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5313
- /* @__PURE__ */ jsx(Globe, { size: 16 }),
5314
- /* @__PURE__ */ jsx("span", { children: t.settings.language })
5641
+ children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-6", children: [
5642
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5643
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5644
+ /* @__PURE__ */ jsx(Globe, { size: 16 }),
5645
+ /* @__PURE__ */ jsx("span", { children: t.settings.language })
5646
+ ] }),
5647
+ /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
5648
+ const isActive = currentLanguage === lang.code;
5649
+ return /* @__PURE__ */ jsxs(
5650
+ "button",
5651
+ {
5652
+ onClick: () => onLanguageChange(lang.code),
5653
+ className: `
5654
+ flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5655
+ ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5656
+ `,
5657
+ children: [
5658
+ /* @__PURE__ */ jsx("span", { children: lang.name }),
5659
+ isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
5660
+ ]
5661
+ },
5662
+ lang.code
5663
+ );
5664
+ }) })
5315
5665
  ] }),
5316
- /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
5317
- const isActive = currentLanguage === lang.code;
5318
- return /* @__PURE__ */ jsxs(
5666
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5667
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5668
+ /* @__PURE__ */ jsx(Zap, { size: 16 }),
5669
+ /* @__PURE__ */ jsx("span", { children: t.settings.haptics })
5670
+ ] }),
5671
+ /* @__PURE__ */ jsxs(
5319
5672
  "button",
5320
5673
  {
5321
- onClick: () => onLanguageChange(lang.code),
5674
+ onClick: onToggleHaptics,
5322
5675
  className: `
5323
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5324
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5325
- `,
5676
+ w-full flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5677
+ ${hapticsEnabled ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5678
+ `,
5326
5679
  children: [
5327
- /* @__PURE__ */ jsx("span", { children: lang.name }),
5328
- isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
5680
+ /* @__PURE__ */ jsx("span", { children: t.settings.enableHaptics }),
5681
+ /* @__PURE__ */ jsx("div", { className: `w-10 h-6 rounded-full p-1 transition-colors ${hapticsEnabled ? "bg-[#00FF41]" : "bg-gray-700"}`, children: /* @__PURE__ */ jsx(
5682
+ "div",
5683
+ {
5684
+ className: `w-4 h-4 rounded-full bg-white transition-transform ${hapticsEnabled ? "translate-x-4" : "translate-x-0"}`
5685
+ }
5686
+ ) })
5329
5687
  ]
5330
- },
5331
- lang.code
5332
- );
5333
- }) })
5334
- ] }) })
5688
+ }
5689
+ )
5690
+ ] })
5691
+ ] })
5335
5692
  }
5336
5693
  );
5337
5694
  }
@@ -5351,6 +5708,7 @@ function GameModals({
5351
5708
  cheats,
5352
5709
  activeCheats,
5353
5710
  onToggleCheat,
5711
+ onAddManualCheat,
5354
5712
  saveModalOpen,
5355
5713
  setSaveModalOpen,
5356
5714
  saveModalMode,
@@ -5370,7 +5728,9 @@ function GameModals({
5370
5728
  settingsModalOpen,
5371
5729
  setSettingsModalOpen,
5372
5730
  currentLanguage,
5373
- onLanguageChange
5731
+ onLanguageChange,
5732
+ hapticsEnabled,
5733
+ onToggleHaptics
5374
5734
  }) {
5375
5735
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5376
5736
  /* @__PURE__ */ jsx(
@@ -5405,6 +5765,7 @@ function GameModals({
5405
5765
  cheats,
5406
5766
  activeCheats,
5407
5767
  onToggle: onToggleCheat,
5768
+ onAddManualCheat,
5408
5769
  onClose: () => {
5409
5770
  setCheatsModalOpen(false);
5410
5771
  onResume();
@@ -5457,7 +5818,9 @@ function GameModals({
5457
5818
  },
5458
5819
  currentLanguage,
5459
5820
  onLanguageChange,
5460
- systemColor
5821
+ systemColor,
5822
+ hapticsEnabled,
5823
+ onToggleHaptics
5461
5824
  }
5462
5825
  )
5463
5826
  ] });
@@ -6752,6 +7115,11 @@ function useEmulatorCore({
6752
7115
  input_volume_up: "add",
6753
7116
  input_volume_down: "subtract",
6754
7117
  input_audio_mute: "f9",
7118
+ // Cheat hotkeys
7119
+ quick_menu_show_cheats: true,
7120
+ input_cheat_index_plus: "y",
7121
+ input_cheat_index_minus: "t",
7122
+ input_cheat_toggle: "u",
6755
7123
  ...inputConfig,
6756
7124
  ...specificConfig,
6757
7125
  // Apply system-specific optimizations
@@ -6845,18 +7213,15 @@ function useEmulatorCore({
6845
7213
  const restart = useCallback(async () => {
6846
7214
  if (nostalgistRef.current) {
6847
7215
  try {
6848
- nostalgistRef.current.restart();
6849
- await new Promise((resolve) => setTimeout(resolve, 100));
6850
- nostalgistRef.current.resume();
6851
- setIsPaused(false);
6852
- setStatus("running");
6853
- } catch (err) {
6854
- console.error("[Nostalgist] Restart error:", err);
7216
+ console.log("[Nostalgist] Full restart - stopping, re-preparing with fresh config, starting");
6855
7217
  stop();
7218
+ await prepare();
6856
7219
  await start();
7220
+ } catch (err) {
7221
+ console.error("[Nostalgist] Restart error:", err);
6857
7222
  }
6858
7223
  }
6859
- }, [stop, start]);
7224
+ }, [stop, prepare, start]);
6860
7225
  const pause = useCallback(() => {
6861
7226
  if (nostalgistRef.current && !isPaused && status === "running") {
6862
7227
  try {
@@ -6949,9 +7314,6 @@ function useEmulatorCore({
6949
7314
  console.error("[Nostalgist] Resize error:", err);
6950
7315
  }
6951
7316
  }, []);
6952
- useCallback(() => {
6953
- return nostalgistRef.current;
6954
- }, []);
6955
7317
  return {
6956
7318
  status,
6957
7319
  setStatus,
@@ -7078,8 +7440,26 @@ function useEmulatorInput({ nostalgistRef }) {
7078
7440
  console.error("[Nostalgist] Press key error:", err);
7079
7441
  }
7080
7442
  }, [nostalgistRef]);
7443
+ const pressDown = useCallback((button) => {
7444
+ if (!nostalgistRef.current) return;
7445
+ try {
7446
+ nostalgistRef.current.pressDown(button);
7447
+ } catch (err) {
7448
+ console.error("[Nostalgist] Press down error:", err);
7449
+ }
7450
+ }, [nostalgistRef]);
7451
+ const pressUp = useCallback((button) => {
7452
+ if (!nostalgistRef.current) return;
7453
+ try {
7454
+ nostalgistRef.current.pressUp(button);
7455
+ } catch (err) {
7456
+ console.error("[Nostalgist] Press up error:", err);
7457
+ }
7458
+ }, [nostalgistRef]);
7081
7459
  return {
7082
- pressKey
7460
+ pressKey,
7461
+ pressDown,
7462
+ pressUp
7083
7463
  };
7084
7464
  }
7085
7465
  var MIN_SAVE_INTERVAL = 100;
@@ -7346,26 +7726,53 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
7346
7726
  stopRewindCapture
7347
7727
  };
7348
7728
  }
7349
- function useEmulatorCheats({ nostalgistRef }) {
7350
- const applyCheat = useCallback((code) => {
7729
+ function useEmulatorCheats({
7730
+ nostalgistRef
7731
+ }) {
7732
+ const allocatedPointersRef = useRef([]);
7733
+ const injectCheats = useCallback((cheats) => {
7351
7734
  if (!nostalgistRef.current) return;
7352
- try {
7353
- nostalgistRef.current.addCheat(code);
7354
- } catch (err) {
7355
- console.error("[Nostalgist] Apply cheat error:", err);
7735
+ const module = nostalgistRef.current.getEmscriptenModule();
7736
+ if (!module) return;
7737
+ if (module._free && allocatedPointersRef.current.length > 0) {
7738
+ allocatedPointersRef.current.forEach((ptr) => {
7739
+ try {
7740
+ module._free(ptr);
7741
+ } catch (e) {
7742
+ }
7743
+ });
7744
+ allocatedPointersRef.current = [];
7356
7745
  }
7357
- }, [nostalgistRef]);
7358
- const resetCheats = useCallback(() => {
7359
- if (!nostalgistRef.current) return;
7360
- try {
7361
- nostalgistRef.current.resetCheats();
7362
- } catch (err) {
7363
- console.error("[Nostalgist] Reset cheats error:", err);
7746
+ if (module._cmd_cheat_realloc) {
7747
+ module._cmd_cheat_realloc(0);
7748
+ if (cheats.length > 0) {
7749
+ module._cmd_cheat_realloc(cheats.length);
7750
+ }
7751
+ }
7752
+ if (cheats.length > 0 && module.stringToNewUTF8 && module._cmd_cheat_set_code) {
7753
+ cheats.forEach((cheat, index) => {
7754
+ try {
7755
+ const ptr = module.stringToNewUTF8(cheat.code);
7756
+ allocatedPointersRef.current.push(ptr);
7757
+ module._cmd_cheat_set_code(index, ptr);
7758
+ if (module._cmd_cheat_toggle_index) {
7759
+ module._cmd_cheat_toggle_index(index, true);
7760
+ }
7761
+ } catch (err) {
7762
+ console.error("[Cheats] Failed to inject cheat:", index, err);
7763
+ }
7764
+ });
7765
+ }
7766
+ if (module._cmd_cheat_apply_cheats) {
7767
+ module._cmd_cheat_apply_cheats();
7364
7768
  }
7365
7769
  }, [nostalgistRef]);
7770
+ const clearCheats = useCallback(() => {
7771
+ injectCheats([]);
7772
+ }, [injectCheats]);
7366
7773
  return {
7367
- applyCheat,
7368
- resetCheats
7774
+ injectCheats,
7775
+ clearCheats
7369
7776
  };
7370
7777
  }
7371
7778
 
@@ -7437,7 +7844,9 @@ var useNostalgist = ({
7437
7844
  initialVolume
7438
7845
  });
7439
7846
  const {
7440
- pressKey
7847
+ pressKey,
7848
+ pressDown,
7849
+ pressUp
7441
7850
  } = useEmulatorInput({
7442
7851
  nostalgistRef
7443
7852
  });
@@ -7460,8 +7869,8 @@ var useNostalgist = ({
7460
7869
  // Disable manual rewind loop for heavy systems
7461
7870
  });
7462
7871
  const {
7463
- applyCheat,
7464
- resetCheats
7872
+ injectCheats,
7873
+ clearCheats
7465
7874
  } = useEmulatorCheats({
7466
7875
  nostalgistRef
7467
7876
  });
@@ -7504,9 +7913,11 @@ var useNostalgist = ({
7504
7913
  toggleMute,
7505
7914
  screenshot,
7506
7915
  pressKey,
7916
+ pressDown,
7917
+ pressUp,
7507
7918
  resize,
7508
- applyCheat,
7509
- resetCheats,
7919
+ injectCheats,
7920
+ clearCheats,
7510
7921
  getNostalgistInstance,
7511
7922
  isPerformanceMode
7512
7923
  }), [
@@ -7536,9 +7947,11 @@ var useNostalgist = ({
7536
7947
  toggleMute,
7537
7948
  screenshot,
7538
7949
  pressKey,
7950
+ pressDown,
7951
+ pressUp,
7539
7952
  resize,
7540
- applyCheat,
7541
- resetCheats,
7953
+ injectCheats,
7954
+ clearCheats,
7542
7955
  getNostalgistInstance,
7543
7956
  isPerformanceMode
7544
7957
  ]);
@@ -8269,39 +8682,95 @@ function useGameSaves({
8269
8682
  handleAutoSaveToggle
8270
8683
  };
8271
8684
  }
8685
+ var generateManualId = () => `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8272
8686
  function useGameCheats({
8273
8687
  nostalgist,
8274
8688
  cheats = [],
8275
- onToggleCheat
8689
+ onToggleCheat,
8690
+ showToast,
8691
+ romId
8276
8692
  }) {
8277
8693
  const [cheatsModalOpen, setCheatsModalOpen] = useState(false);
8278
8694
  const [activeCheats, setActiveCheats] = useState(/* @__PURE__ */ new Set());
8695
+ const [manualCheatsInternal, setManualCheatsInternal] = useState([]);
8696
+ const [isLoaded, setIsLoaded] = useState(false);
8697
+ const cheatStorageKey = romId ? `koin_cheats_${romId}` : null;
8698
+ useEffect(() => {
8699
+ if (!cheatStorageKey) return;
8700
+ setIsLoaded(false);
8701
+ try {
8702
+ const stored = localStorage.getItem(cheatStorageKey);
8703
+ if (stored) {
8704
+ const parsed = JSON.parse(stored);
8705
+ if (Array.isArray(parsed)) {
8706
+ setManualCheatsInternal(parsed);
8707
+ }
8708
+ }
8709
+ } catch (e) {
8710
+ console.error("[Cheats] Failed to load cheats:", e);
8711
+ } finally {
8712
+ setIsLoaded(true);
8713
+ }
8714
+ }, [cheatStorageKey]);
8715
+ useEffect(() => {
8716
+ if (!cheatStorageKey || !isLoaded) return;
8717
+ localStorage.setItem(cheatStorageKey, JSON.stringify(manualCheatsInternal));
8718
+ }, [manualCheatsInternal, cheatStorageKey, isLoaded]);
8719
+ const allCheats = useMemo(() => {
8720
+ const normalizedExternal = cheats.map((c) => ({
8721
+ id: typeof c.id === "number" ? `db-${c.id}` : c.id,
8722
+ code: c.code,
8723
+ description: c.description,
8724
+ source: "database"
8725
+ }));
8726
+ return [...normalizedExternal, ...manualCheatsInternal];
8727
+ }, [cheats, manualCheatsInternal]);
8728
+ const handleAddManualCheat = (code, description) => {
8729
+ if (!nostalgist) return;
8730
+ const newCheat = {
8731
+ id: generateManualId(),
8732
+ code,
8733
+ description,
8734
+ source: "manual"
8735
+ };
8736
+ setManualCheatsInternal((prev) => [...prev, newCheat]);
8737
+ setActiveCheats((prev) => new Set(prev).add(newCheat.id));
8738
+ setCheatsModalOpen(false);
8739
+ const currentActiveCheats = [...activeCheats, newCheat.id];
8740
+ const cheatsToInject = allCheats.filter((c) => currentActiveCheats.includes(c.id) || c.id === newCheat.id).concat([newCheat]);
8741
+ nostalgist.injectCheats(cheatsToInject.map((c) => ({ code: c.code })));
8742
+ nostalgist.resume();
8743
+ showToast?.("Cheat added!", "success");
8744
+ };
8279
8745
  const handleToggleCheat = (cheatId) => {
8280
8746
  if (!nostalgist) return;
8281
8747
  const newActiveCheats = new Set(activeCheats);
8282
8748
  const isActive = newActiveCheats.has(cheatId);
8283
8749
  if (isActive) {
8284
8750
  newActiveCheats.delete(cheatId);
8751
+ showToast?.("Cheat Disabled");
8285
8752
  } else {
8286
8753
  newActiveCheats.add(cheatId);
8754
+ showToast?.("Cheat Enabled", "success");
8287
8755
  }
8288
8756
  setActiveCheats(newActiveCheats);
8289
- onToggleCheat?.(cheatId, !isActive);
8290
- nostalgist.resetCheats();
8291
- setTimeout(() => {
8292
- newActiveCheats.forEach((id) => {
8293
- const cheat = cheats.find((c) => c.id === id);
8294
- if (cheat) {
8295
- nostalgist.applyCheat(cheat.code);
8296
- }
8297
- });
8298
- }, 50);
8757
+ if (onToggleCheat) {
8758
+ const numericId = cheatId.startsWith("db-") ? parseInt(cheatId.slice(3), 10) : void 0;
8759
+ if (numericId !== void 0) {
8760
+ onToggleCheat(numericId, !isActive);
8761
+ }
8762
+ }
8763
+ const cheatsToInject = allCheats.filter((c) => newActiveCheats.has(c.id)).map((c) => ({ code: c.code }));
8764
+ nostalgist.injectCheats(cheatsToInject);
8299
8765
  };
8300
8766
  return {
8301
8767
  cheatsModalOpen,
8302
8768
  setCheatsModalOpen,
8303
8769
  activeCheats,
8304
- handleToggleCheat
8770
+ allCheats,
8771
+ // Unified list - replaces separate cheats + manualCheats
8772
+ handleToggleCheat,
8773
+ handleAddManualCheat
8305
8774
  };
8306
8775
  }
8307
8776
  function useGameRecording({
@@ -8478,10 +8947,13 @@ function useGamePlayer(props) {
8478
8947
  cheatsModalOpen,
8479
8948
  setCheatsModalOpen,
8480
8949
  activeCheats,
8481
- handleToggleCheat
8950
+ allCheats,
8951
+ handleToggleCheat,
8952
+ handleAddManualCheat
8482
8953
  } = useGameCheats({
8483
8954
  ...props,
8484
- nostalgist
8955
+ nostalgist,
8956
+ showToast
8485
8957
  });
8486
8958
  const {
8487
8959
  isRecording,
@@ -8537,7 +9009,9 @@ function useGamePlayer(props) {
8537
9009
  hardcoreRestrictions,
8538
9010
  // Cheats
8539
9011
  activeCheats,
9012
+ allCheats,
8540
9013
  handleToggleCheat,
9014
+ handleAddManualCheat,
8541
9015
  // Emulator
8542
9016
  nostalgist,
8543
9017
  volumeState,
@@ -8565,7 +9039,8 @@ var DEFAULT_SETTINGS = {
8565
9039
  muted: false,
8566
9040
  shader: "",
8567
9041
  showPerformanceOverlay: false,
8568
- showInputDisplay: false
9042
+ showInputDisplay: false,
9043
+ hapticsEnabled: true
8569
9044
  };
8570
9045
  function usePlayerPersistence(onSettingsChange) {
8571
9046
  const [settings, setSettings] = useState(DEFAULT_SETTINGS);
@@ -8672,7 +9147,9 @@ var es = {
8672
9147
  shortcuts: "Atajos",
8673
9148
  exit: "Salir",
8674
9149
  language: "Idioma",
8675
- selectLanguage: "Seleccionar idioma"
9150
+ selectLanguage: "Seleccionar idioma",
9151
+ haptics: "Vibraci\xF3n",
9152
+ enableHaptics: "Habilitar vibraci\xF3n"
8676
9153
  },
8677
9154
  overlay: {
8678
9155
  play: "JUGAR",
@@ -8755,7 +9232,10 @@ var es = {
8755
9232
  emptyDesc: "No se encontraron c\xF3digos para este juego",
8756
9233
  copy: "Copiar c\xF3digo",
8757
9234
  active: "{{count}} truco{{s}} activo(s)",
8758
- toggleHint: "Haz clic para activar/desactivar"
9235
+ toggleHint: "Haz clic para activar/desactivar",
9236
+ codePlaceholder: "Introduce c\xF3digo (ej: 00C-048-E6E)",
9237
+ descPlaceholder: "Descripci\xF3n (opcional)",
9238
+ add: "A\xF1adir truco"
8759
9239
  },
8760
9240
  saveSlots: {
8761
9241
  title: "Guardar partida",
@@ -8900,7 +9380,9 @@ var fr = {
8900
9380
  shortcuts: "Raccourcis",
8901
9381
  exit: "Quitter",
8902
9382
  language: "Langue",
8903
- selectLanguage: "Choisir la langue"
9383
+ selectLanguage: "Choisir la langue",
9384
+ haptics: "Vibration",
9385
+ enableHaptics: "Activer la vibration"
8904
9386
  },
8905
9387
  overlay: {
8906
9388
  play: "JOUER",
@@ -8983,7 +9465,10 @@ var fr = {
8983
9465
  emptyDesc: "Aucun code trouv\xE9 pour ce jeu",
8984
9466
  copy: "Copier",
8985
9467
  active: "{{count}} actif(s)",
8986
- toggleHint: "Cliquez pour activer/d\xE9sactiver"
9468
+ toggleHint: "Cliquez pour activer/d\xE9sactiver",
9469
+ codePlaceholder: "Entrez le code (ex: 00C-048-E6E)",
9470
+ descPlaceholder: "Description (optionnel)",
9471
+ add: "Ajouter"
8987
9472
  },
8988
9473
  saveSlots: {
8989
9474
  title: "Sauvegardes",
@@ -9156,7 +9641,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9156
9641
  hardcoreRestrictions,
9157
9642
  // Cheats
9158
9643
  activeCheats,
9644
+ allCheats,
9159
9645
  handleToggleCheat,
9646
+ handleAddManualCheat,
9160
9647
  // Emulator Instance & State
9161
9648
  nostalgist,
9162
9649
  // Contains start, restart, etc.
@@ -9221,7 +9708,7 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9221
9708
  if (muted !== settings.muted) toggleMute();
9222
9709
  }
9223
9710
  }, [settingsLoaded]);
9224
- const { system, systemColor = "#00FF41", cheats = [], onExit } = props;
9711
+ const { system, systemColor = "#00FF41", onExit } = props;
9225
9712
  const handlePauseToggle = useCallback(() => {
9226
9713
  status === "ready" ? start() : togglePause();
9227
9714
  }, [status, start, togglePause]);
@@ -9277,6 +9764,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9277
9764
  const handleToggleInputDisplay = useCallback(() => {
9278
9765
  updateSettings({ showInputDisplay: !settings.showInputDisplay });
9279
9766
  }, [updateSettings, settings.showInputDisplay]);
9767
+ const handleToggleHaptics = useCallback(() => {
9768
+ updateSettings({ hapticsEnabled: !settings.hapticsEnabled });
9769
+ }, [updateSettings, settings.hapticsEnabled]);
9280
9770
  const handleToggleRecording = useCallback(async () => {
9281
9771
  if (!recordingSupported) {
9282
9772
  console.warn("[Recording] Not supported in this browser");
@@ -9363,7 +9853,12 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9363
9853
  system,
9364
9854
  isRunning: status === "running" || status === "paused",
9365
9855
  controls,
9366
- systemColor
9856
+ systemColor,
9857
+ hapticsEnabled: settings.hapticsEnabled,
9858
+ onButtonDown: nostalgist.pressDown,
9859
+ onButtonUp: nostalgist.pressUp,
9860
+ onPause: nostalgist.pause,
9861
+ onResume: nostalgist.resume
9367
9862
  }
9368
9863
  ),
9369
9864
  !isFullscreen2 && isMobile && /* @__PURE__ */ jsx(
@@ -9518,9 +10013,10 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9518
10013
  systemColor,
9519
10014
  cheatsModalOpen,
9520
10015
  setCheatsModalOpen,
9521
- cheats,
10016
+ cheats: allCheats,
9522
10017
  activeCheats,
9523
10018
  onToggleCheat: handleToggleCheat,
10019
+ onAddManualCheat: handleAddManualCheat,
9524
10020
  saveModalOpen,
9525
10021
  setSaveModalOpen,
9526
10022
  saveModalMode,
@@ -9540,7 +10036,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9540
10036
  settingsModalOpen,
9541
10037
  setSettingsModalOpen,
9542
10038
  currentLanguage: props.currentLanguage,
9543
- onLanguageChange: props.onLanguageChange
10039
+ onLanguageChange: props.onLanguageChange,
10040
+ hapticsEnabled: settings.hapticsEnabled,
10041
+ onToggleHaptics: handleToggleHaptics
9544
10042
  }
9545
10043
  ),
9546
10044
  !isMobile && /* @__PURE__ */ jsx(