koin.js 1.0.15 → 1.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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",
@@ -821,7 +826,7 @@ var SaveLoadControls = memo(function SaveLoadControls2({
821
826
  {
822
827
  progress: autoSaveProgress,
823
828
  state: autoSavePaused ? "idle" : autoSaveState,
824
- intervalSeconds: 20,
829
+ intervalSeconds: 60,
825
830
  isPaused: autoSavePaused,
826
831
  onClick: onAutoSaveToggle
827
832
  }
@@ -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,55 +2956,371 @@ 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,
2989
- // RT / R2
2990
- // System
2991
- select: 8,
2992
- // Back / Select / Share
2993
- start: 9,
2994
- // Start / Options
2995
- // Stick clicks
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,
3317
+ // RT / R2
3318
+ // System
3319
+ select: 8,
3320
+ // Back / Select / Share
3321
+ start: 9,
3322
+ // Start / Options
3323
+ // Stick clicks
2996
3324
  l3: 10,
2997
3325
  // Left stick click
2998
3326
  r3: 11,
@@ -3535,343 +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 {
3705
- const rect = dpadRef.current?.getBoundingClientRect();
3706
- if (rect) {
3707
- drag.clearDragTimer();
3708
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3709
- }
3710
- }
3711
- }, [drag, getDirectionsFromTouch, updateDirections]);
3712
- const handleTouchEnd = useCallback((e) => {
3713
- e.preventDefault();
3714
- drag.clearDragTimer();
3715
- let touchEnded = false;
3716
- for (let i = 0; i < e.changedTouches.length; i++) {
3717
- if (e.changedTouches[i].identifier === activeTouchRef.current) {
3718
- touchEnded = true;
3719
- break;
3720
- }
3721
- }
3722
- if (touchEnded) {
3723
- activeTouchRef.current = null;
3724
- if (drag.isDragging) {
3725
- drag.handleDragEnd();
3726
- } else {
3727
- activeDirectionsRef.current.forEach((dir) => {
3728
- const keyCode = getKeyCode(dir);
3729
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3730
- });
3731
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3732
- updateVisuals(/* @__PURE__ */ new Set());
3733
- }
3734
- }
3735
- }, [getKeyCode, updateVisuals, drag]);
3736
- useTouchEvents(dpadRef, {
3737
- onTouchStart: handleTouchStart,
3738
- onTouchMove: handleTouchMove,
3739
- onTouchEnd: handleTouchEnd,
3740
- onTouchCancel: handleTouchEnd
3741
- }, { cleanup: drag.clearDragTimer });
3742
- const leftPx = displayX / 100 * containerWidth - size / 2;
3743
- const topPx = displayY / 100 * containerHeight - size / 2;
3744
- const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
3745
- const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
3746
- const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
3747
- const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
3748
- return /* @__PURE__ */ jsxs(
3749
- "div",
3750
- {
3751
- ref: dpadRef,
3752
- className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
3753
- style: {
3754
- top: 0,
3755
- left: 0,
3756
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
3757
- width: size,
3758
- height: size,
3759
- opacity: isLandscape ? 0.75 : 0.9,
3760
- WebkitTouchCallout: "none",
3761
- WebkitUserSelect: "none",
3762
- touchAction: "none",
3763
- transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
3764
- },
3765
- children: [
3766
- /* @__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"}` }),
3767
- /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3768
- /* @__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" }),
3769
- /* @__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" }),
3770
- /* @__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" }),
3771
- /* @__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" }),
3772
- /* @__PURE__ */ jsx(
3773
- "circle",
3774
- {
3775
- ref: centerCircleRef,
3776
- cx: "50",
3777
- cy: "50",
3778
- r: "12",
3779
- fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
3780
- stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3781
- strokeWidth: drag.isDragging ? 2 : 1
3782
- }
3783
- ),
3784
- /* @__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" }),
3785
- /* @__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" }),
3786
- /* @__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" }),
3787
- /* @__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" })
3788
- ] })
3789
- ]
3790
- }
3791
- );
3792
- });
3793
- var Dpad_default = Dpad;
3794
-
3795
- // src/components/VirtualController/positioning.ts
3796
- function adjustButtonPosition(config, context) {
3797
- const sizeMultiplier = context.isFullscreen ? 1.1 : 1;
3798
- return {
3799
- ...config,
3800
- size: Math.floor(config.size * sizeMultiplier)
3801
- };
3802
- }
3803
- var STORAGE_KEY = "virtual-button-positions";
3804
- function useButtonPositions() {
3805
- const [landscapePositions, setLandscapePositions] = useState({});
3806
- const [portraitPositions, setPortraitPositions] = useState({});
3807
- useEffect(() => {
3808
- try {
3809
- const stored = localStorage.getItem(STORAGE_KEY);
3810
- if (stored) {
3811
- const parsed = JSON.parse(stored);
3812
- if (parsed.landscape && parsed.portrait) {
3813
- setLandscapePositions(parsed.landscape);
3814
- setPortraitPositions(parsed.portrait);
3815
- } else {
3816
- setLandscapePositions(parsed);
3817
- }
3818
- }
3819
- } catch (e) {
3820
- console.error("Failed to load button positions:", e);
3821
- }
3822
- }, []);
3823
- const savePosition = useCallback((buttonType, x, y, isLandscape) => {
3824
- if (isLandscape) {
3825
- setLandscapePositions((prev) => {
3826
- const updated = { ...prev, [buttonType]: { x, y } };
3827
- try {
3828
- const stored = {
3829
- landscape: updated,
3830
- portrait: portraitPositions
3831
- };
3832
- localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
3833
- } catch (e) {
3834
- console.error("Failed to save button position:", e);
3835
- }
3836
- return updated;
3837
- });
3838
- } else {
3839
- setPortraitPositions((prev) => {
3840
- const updated = { ...prev, [buttonType]: { x, y } };
3841
- try {
3842
- const stored = {
3843
- landscape: landscapePositions,
3844
- portrait: updated
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
- }
3853
- }, [landscapePositions, portraitPositions]);
3854
- const getPosition = useCallback((buttonType, isLandscape) => {
3855
- const positions = isLandscape ? landscapePositions : portraitPositions;
3856
- return positions[buttonType] || null;
3857
- }, [landscapePositions, portraitPositions]);
3858
- const resetPositions = useCallback(() => {
3859
- setLandscapePositions({});
3860
- setPortraitPositions({});
3861
- try {
3862
- localStorage.removeItem(STORAGE_KEY);
3863
- } catch (e) {
3864
- console.error("Failed to reset button positions:", e);
3865
- }
3866
- }, []);
3867
- return {
3868
- landscapePositions,
3869
- portraitPositions,
3870
- savePosition,
3871
- getPosition,
3872
- resetPositions
3873
- };
3874
- }
3875
3866
  var STORAGE_KEY2 = "koin-controls-hint-shown";
3876
3867
  function ControlsHint({ isVisible }) {
3877
3868
  const [show, setShow] = useState(false);
@@ -3909,8 +3900,16 @@ function ControlsHint({ isVisible }) {
3909
3900
  children: [
3910
3901
  /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsx(Move, { size: 32, className: "text-green-400" }) }) }),
3911
3902
  /* @__PURE__ */ jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
3912
- /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-4", children: [
3913
- /* @__PURE__ */ jsx("strong", { className: "text-white", children: "Long-press" }),
3903
+ /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3904
+ "Use the ",
3905
+ /* @__PURE__ */ jsx(Lock, { size: 12, className: "inline mx-1 text-white" }),
3906
+ " ",
3907
+ /* @__PURE__ */ jsx("strong", { className: "text-white", children: "lock icon" }),
3908
+ " at the top to unlock controls for repositioning."
3909
+ ] }),
3910
+ /* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
3911
+ "When unlocked, ",
3912
+ /* @__PURE__ */ jsx("strong", { className: "text-white", children: "long-press" }),
3914
3913
  " any button or the ",
3915
3914
  /* @__PURE__ */ jsx("strong", { className: "text-white", children: "D-pad center" }),
3916
3915
  " to drag and reposition it."
@@ -3948,7 +3947,7 @@ function LockButton({
3948
3947
  "button",
3949
3948
  {
3950
3949
  onClick: onToggle,
3951
- 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",
3952
3951
  style: {
3953
3952
  backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
3954
3953
  border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
@@ -3962,18 +3961,164 @@ function LockButton({
3962
3961
  }
3963
3962
  )
3964
3963
  }
3965
- );
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
+ ) });
3966
4103
  }
3967
4104
  var LOCK_KEY = "koin-controls-locked";
3968
4105
  function VirtualController({
3969
4106
  system,
3970
4107
  isRunning,
3971
4108
  controls,
3972
- systemColor = "#00FF41"
4109
+ systemColor = "#00FF41",
3973
4110
  // Default retro green
4111
+ hapticsEnabled = true,
4112
+ onButtonDown,
4113
+ onButtonUp,
4114
+ onPause,
4115
+ onResume
3974
4116
  }) {
3975
4117
  const { isMobile, isLandscape, isPortrait } = useMobile();
3976
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);
3977
4122
  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
3978
4123
  const [isFullscreenState, setIsFullscreenState] = useState(false);
3979
4124
  const [isLocked, setIsLocked] = useState(true);
@@ -3991,6 +4136,28 @@ function VirtualController({
3991
4136
  return newValue;
3992
4137
  });
3993
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]);
3994
4161
  const layout = getLayoutForSystem(system);
3995
4162
  const visibleButtons = layout.buttons.filter((btn) => {
3996
4163
  if (isPortrait) {
@@ -4060,9 +4227,9 @@ function VirtualController({
4060
4227
  return;
4061
4228
  }
4062
4229
  setPressedButtons((prev) => new Set(prev).add(buttonType));
4063
- dispatchKeyboardEvent("keydown", keyboardCode);
4230
+ onButtonDown(buttonType);
4064
4231
  setTimeout(() => {
4065
- dispatchKeyboardEvent("keyup", keyboardCode);
4232
+ onButtonUp(buttonType);
4066
4233
  setPressedButtons((prev) => {
4067
4234
  const next = new Set(prev);
4068
4235
  next.delete(buttonType);
@@ -4070,7 +4237,7 @@ function VirtualController({
4070
4237
  });
4071
4238
  }, 100);
4072
4239
  },
4073
- [isRunning, getButtonKeyboardCode]
4240
+ [isRunning, getButtonKeyboardCode, onButtonDown, onButtonUp]
4074
4241
  );
4075
4242
  const handlePressDown = useCallback(
4076
4243
  (buttonType) => {
@@ -4079,15 +4246,70 @@ function VirtualController({
4079
4246
  if (isSystemButton) return;
4080
4247
  const keyboardCode = getButtonKeyboardCode(buttonType);
4081
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
+ }
4082
4302
  setPressedButtons((prev) => {
4083
4303
  if (prev.has(buttonType)) return prev;
4084
4304
  const next = new Set(prev);
4085
4305
  next.add(buttonType);
4086
4306
  return next;
4087
4307
  });
4088
- dispatchKeyboardEvent("keydown", keyboardCode);
4308
+ if (!heldButtons.has(buttonType)) {
4309
+ onButtonDown(buttonType);
4310
+ }
4089
4311
  },
4090
- [isRunning, getButtonKeyboardCode]
4312
+ [isRunning, getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonDown, onButtonUp]
4091
4313
  );
4092
4314
  const handleRelease = useCallback(
4093
4315
  (buttonType) => {
@@ -4095,15 +4317,29 @@ function VirtualController({
4095
4317
  if (isSystemButton) return;
4096
4318
  const keyboardCode = getButtonKeyboardCode(buttonType);
4097
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;
4098
4334
  setPressedButtons((prev) => {
4099
4335
  if (!prev.has(buttonType)) return prev;
4100
4336
  const next = new Set(prev);
4101
4337
  next.delete(buttonType);
4102
4338
  return next;
4103
4339
  });
4104
- dispatchKeyboardEvent("keyup", keyboardCode);
4340
+ onButtonUp(buttonType);
4105
4341
  },
4106
- [getButtonKeyboardCode]
4342
+ [getButtonKeyboardCode, isHoldMode, isTurboMode, heldButtons, hapticsEnabled, onButtonUp]
4107
4343
  );
4108
4344
  useEffect(() => {
4109
4345
  if (!isRunning && pressedButtons.size > 0) {
@@ -4115,6 +4351,22 @@ function VirtualController({
4115
4351
  setPressedButtons(/* @__PURE__ */ new Set());
4116
4352
  }
4117
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]);
4118
4370
  const memoizedButtonElements = useMemo(() => {
4119
4371
  const width = containerSize.width || (typeof window !== "undefined" ? window.innerWidth : 0);
4120
4372
  const height = containerSize.height || (typeof window !== "undefined" ? window.innerHeight : 0);
@@ -4145,14 +4397,32 @@ function VirtualController({
4145
4397
  className: "fixed inset-0 z-30 pointer-events-none",
4146
4398
  style: { touchAction: "none" },
4147
4399
  children: [
4148
- /* @__PURE__ */ jsx(
4149
- LockButton,
4150
- {
4151
- isLocked,
4152
- onToggle: toggleLock,
4153
- systemColor
4154
- }
4155
- ),
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
+ ] }),
4156
4426
  /* @__PURE__ */ jsx(
4157
4427
  Dpad_default,
4158
4428
  {
@@ -4161,18 +4431,20 @@ function VirtualController({
4161
4431
  y: finalDpadY,
4162
4432
  containerWidth: containerSize.width || window.innerWidth,
4163
4433
  containerHeight: containerSize.height || window.innerHeight,
4164
- controls,
4165
4434
  systemColor,
4166
4435
  isLandscape,
4167
4436
  customPosition: getPosition("up", isLandscape),
4168
- 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
4169
4441
  }
4170
4442
  ),
4171
4443
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
4172
4444
  VirtualButton_default,
4173
4445
  {
4174
4446
  config: adjustedConfig,
4175
- isPressed: pressedButtons.has(buttonConfig.type),
4447
+ isPressed: pressedButtons.has(buttonConfig.type) || heldButtons.has(buttonConfig.type),
4176
4448
  onPress: handlePress,
4177
4449
  onPressDown: handlePressDown,
4178
4450
  onRelease: handleRelease,
@@ -4181,10 +4453,24 @@ function VirtualController({
4181
4453
  customPosition,
4182
4454
  onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
4183
4455
  isLandscape,
4184
- 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)
4185
4461
  },
4186
4462
  buttonConfig.type
4187
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
+ ),
4188
4474
  /* @__PURE__ */ jsx(ControlsHint, { isVisible: isRunning })
4189
4475
  ]
4190
4476
  }
@@ -4871,7 +5157,8 @@ function CheatModal({
4871
5157
  cheats,
4872
5158
  activeCheats,
4873
5159
  onToggle,
4874
- onClose
5160
+ onClose,
5161
+ onAddManualCheat
4875
5162
  }) {
4876
5163
  const t = useKoinTranslation();
4877
5164
  const [copiedId, setCopiedId] = React2.useState(null);
@@ -4889,54 +5176,111 @@ function CheatModal({
4889
5176
  subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
4890
5177
  icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
4891
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 }),
4892
- 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: [
4893
- /* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
4894
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
4895
- /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
4896
- ] }) : cheats.map((cheat) => {
4897
- const isActive = activeCheats.has(cheat.id);
4898
- return /* @__PURE__ */ jsxs(
4899
- "div",
4900
- {
4901
- className: `
4902
- group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
4903
- ${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
4904
- `,
4905
- onClick: () => onToggle(cheat.id),
4906
- children: [
4907
- /* @__PURE__ */ jsx(
4908
- "div",
4909
- {
4910
- className: `
4911
- flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
4912
- ${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
4913
- `,
4914
- 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
+ }
4915
5198
  }
4916
- ),
4917
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
4918
- /* @__PURE__ */ jsx("p", { className: `font-medium ${isActive ? "text-purple-300" : "text-white"}`, children: cheat.description }),
4919
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
4920
- /* @__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 }),
4921
- /* @__PURE__ */ jsx(
4922
- "button",
4923
- {
4924
- onClick: (e) => {
4925
- e.stopPropagation();
4926
- handleCopy(cheat.code, cheat.id);
4927
- },
4928
- className: "p-1.5 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors",
4929
- title: t.modals.cheats.copy,
4930
- children: copiedId === cheat.id ? /* @__PURE__ */ jsx(Check, { size: 14, className: "text-green-400" }) : /* @__PURE__ */ jsx(Copy, { size: 14 })
4931
- }
4932
- )
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
+ ] })
4933
5277
  ] })
4934
- ] })
4935
- ]
4936
- },
4937
- cheat.id
4938
- );
4939
- }) })
5278
+ ]
5279
+ },
5280
+ cheat.id
5281
+ );
5282
+ }) })
5283
+ ] })
4940
5284
  }
4941
5285
  );
4942
5286
  }
@@ -5268,7 +5612,9 @@ function SettingsModal({
5268
5612
  onClose,
5269
5613
  currentLanguage,
5270
5614
  onLanguageChange,
5271
- systemColor = "#00FF41"
5615
+ systemColor = "#00FF41",
5616
+ hapticsEnabled,
5617
+ onToggleHaptics
5272
5618
  }) {
5273
5619
  const t = useKoinTranslation();
5274
5620
  const languages = [
@@ -5292,30 +5638,57 @@ function SettingsModal({
5292
5638
  children: t.modals.shortcuts.pressEsc
5293
5639
  }
5294
5640
  ),
5295
- children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5296
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
5297
- /* @__PURE__ */ jsx(Globe, { size: 16 }),
5298
- /* @__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
+ }) })
5299
5665
  ] }),
5300
- /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: languages.map((lang) => {
5301
- const isActive = currentLanguage === lang.code;
5302
- 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(
5303
5672
  "button",
5304
5673
  {
5305
- onClick: () => onLanguageChange(lang.code),
5674
+ onClick: onToggleHaptics,
5306
5675
  className: `
5307
- flex items-center justify-between px-4 py-3 rounded-lg border transition-all
5308
- ${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
5309
- `,
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
+ `,
5310
5679
  children: [
5311
- /* @__PURE__ */ jsx("span", { children: lang.name }),
5312
- 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
+ ) })
5313
5687
  ]
5314
- },
5315
- lang.code
5316
- );
5317
- }) })
5318
- ] }) })
5688
+ }
5689
+ )
5690
+ ] })
5691
+ ] })
5319
5692
  }
5320
5693
  );
5321
5694
  }
@@ -5335,6 +5708,7 @@ function GameModals({
5335
5708
  cheats,
5336
5709
  activeCheats,
5337
5710
  onToggleCheat,
5711
+ onAddManualCheat,
5338
5712
  saveModalOpen,
5339
5713
  setSaveModalOpen,
5340
5714
  saveModalMode,
@@ -5354,7 +5728,9 @@ function GameModals({
5354
5728
  settingsModalOpen,
5355
5729
  setSettingsModalOpen,
5356
5730
  currentLanguage,
5357
- onLanguageChange
5731
+ onLanguageChange,
5732
+ hapticsEnabled,
5733
+ onToggleHaptics
5358
5734
  }) {
5359
5735
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5360
5736
  /* @__PURE__ */ jsx(
@@ -5389,6 +5765,7 @@ function GameModals({
5389
5765
  cheats,
5390
5766
  activeCheats,
5391
5767
  onToggle: onToggleCheat,
5768
+ onAddManualCheat,
5392
5769
  onClose: () => {
5393
5770
  setCheatsModalOpen(false);
5394
5771
  onResume();
@@ -5441,7 +5818,9 @@ function GameModals({
5441
5818
  },
5442
5819
  currentLanguage,
5443
5820
  onLanguageChange,
5444
- systemColor
5821
+ systemColor,
5822
+ hapticsEnabled,
5823
+ onToggleHaptics
5445
5824
  }
5446
5825
  )
5447
5826
  ] });
@@ -6736,6 +7115,11 @@ function useEmulatorCore({
6736
7115
  input_volume_up: "add",
6737
7116
  input_volume_down: "subtract",
6738
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",
6739
7123
  ...inputConfig,
6740
7124
  ...specificConfig,
6741
7125
  // Apply system-specific optimizations
@@ -6829,18 +7213,15 @@ function useEmulatorCore({
6829
7213
  const restart = useCallback(async () => {
6830
7214
  if (nostalgistRef.current) {
6831
7215
  try {
6832
- nostalgistRef.current.restart();
6833
- await new Promise((resolve) => setTimeout(resolve, 100));
6834
- nostalgistRef.current.resume();
6835
- setIsPaused(false);
6836
- setStatus("running");
6837
- } catch (err) {
6838
- console.error("[Nostalgist] Restart error:", err);
7216
+ console.log("[Nostalgist] Full restart - stopping, re-preparing with fresh config, starting");
6839
7217
  stop();
7218
+ await prepare();
6840
7219
  await start();
7220
+ } catch (err) {
7221
+ console.error("[Nostalgist] Restart error:", err);
6841
7222
  }
6842
7223
  }
6843
- }, [stop, start]);
7224
+ }, [stop, prepare, start]);
6844
7225
  const pause = useCallback(() => {
6845
7226
  if (nostalgistRef.current && !isPaused && status === "running") {
6846
7227
  try {
@@ -6933,9 +7314,6 @@ function useEmulatorCore({
6933
7314
  console.error("[Nostalgist] Resize error:", err);
6934
7315
  }
6935
7316
  }, []);
6936
- useCallback(() => {
6937
- return nostalgistRef.current;
6938
- }, []);
6939
7317
  return {
6940
7318
  status,
6941
7319
  setStatus,
@@ -7062,8 +7440,26 @@ function useEmulatorInput({ nostalgistRef }) {
7062
7440
  console.error("[Nostalgist] Press key error:", err);
7063
7441
  }
7064
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]);
7065
7459
  return {
7066
- pressKey
7460
+ pressKey,
7461
+ pressDown,
7462
+ pressUp
7067
7463
  };
7068
7464
  }
7069
7465
  var MIN_SAVE_INTERVAL = 100;
@@ -7330,26 +7726,53 @@ function useEmulatorSaves({ nostalgistRef, isPaused, setIsPaused, setStatus, rew
7330
7726
  stopRewindCapture
7331
7727
  };
7332
7728
  }
7333
- function useEmulatorCheats({ nostalgistRef }) {
7334
- const applyCheat = useCallback((code) => {
7729
+ function useEmulatorCheats({
7730
+ nostalgistRef
7731
+ }) {
7732
+ const allocatedPointersRef = useRef([]);
7733
+ const injectCheats = useCallback((cheats) => {
7335
7734
  if (!nostalgistRef.current) return;
7336
- try {
7337
- nostalgistRef.current.addCheat(code);
7338
- } catch (err) {
7339
- 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 = [];
7340
7745
  }
7341
- }, [nostalgistRef]);
7342
- const resetCheats = useCallback(() => {
7343
- if (!nostalgistRef.current) return;
7344
- try {
7345
- nostalgistRef.current.resetCheats();
7346
- } catch (err) {
7347
- 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();
7348
7768
  }
7349
7769
  }, [nostalgistRef]);
7770
+ const clearCheats = useCallback(() => {
7771
+ injectCheats([]);
7772
+ }, [injectCheats]);
7350
7773
  return {
7351
- applyCheat,
7352
- resetCheats
7774
+ injectCheats,
7775
+ clearCheats
7353
7776
  };
7354
7777
  }
7355
7778
 
@@ -7421,7 +7844,9 @@ var useNostalgist = ({
7421
7844
  initialVolume
7422
7845
  });
7423
7846
  const {
7424
- pressKey
7847
+ pressKey,
7848
+ pressDown,
7849
+ pressUp
7425
7850
  } = useEmulatorInput({
7426
7851
  nostalgistRef
7427
7852
  });
@@ -7444,8 +7869,8 @@ var useNostalgist = ({
7444
7869
  // Disable manual rewind loop for heavy systems
7445
7870
  });
7446
7871
  const {
7447
- applyCheat,
7448
- resetCheats
7872
+ injectCheats,
7873
+ clearCheats
7449
7874
  } = useEmulatorCheats({
7450
7875
  nostalgistRef
7451
7876
  });
@@ -7488,9 +7913,11 @@ var useNostalgist = ({
7488
7913
  toggleMute,
7489
7914
  screenshot,
7490
7915
  pressKey,
7916
+ pressDown,
7917
+ pressUp,
7491
7918
  resize,
7492
- applyCheat,
7493
- resetCheats,
7919
+ injectCheats,
7920
+ clearCheats,
7494
7921
  getNostalgistInstance,
7495
7922
  isPerformanceMode
7496
7923
  }), [
@@ -7520,9 +7947,11 @@ var useNostalgist = ({
7520
7947
  toggleMute,
7521
7948
  screenshot,
7522
7949
  pressKey,
7950
+ pressDown,
7951
+ pressUp,
7523
7952
  resize,
7524
- applyCheat,
7525
- resetCheats,
7953
+ injectCheats,
7954
+ clearCheats,
7526
7955
  getNostalgistInstance,
7527
7956
  isPerformanceMode
7528
7957
  ]);
@@ -8253,39 +8682,95 @@ function useGameSaves({
8253
8682
  handleAutoSaveToggle
8254
8683
  };
8255
8684
  }
8685
+ var generateManualId = () => `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8256
8686
  function useGameCheats({
8257
8687
  nostalgist,
8258
8688
  cheats = [],
8259
- onToggleCheat
8689
+ onToggleCheat,
8690
+ showToast,
8691
+ romId
8260
8692
  }) {
8261
8693
  const [cheatsModalOpen, setCheatsModalOpen] = useState(false);
8262
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
+ };
8263
8745
  const handleToggleCheat = (cheatId) => {
8264
8746
  if (!nostalgist) return;
8265
8747
  const newActiveCheats = new Set(activeCheats);
8266
8748
  const isActive = newActiveCheats.has(cheatId);
8267
8749
  if (isActive) {
8268
8750
  newActiveCheats.delete(cheatId);
8751
+ showToast?.("Cheat Disabled");
8269
8752
  } else {
8270
8753
  newActiveCheats.add(cheatId);
8754
+ showToast?.("Cheat Enabled", "success");
8271
8755
  }
8272
8756
  setActiveCheats(newActiveCheats);
8273
- onToggleCheat?.(cheatId, !isActive);
8274
- nostalgist.resetCheats();
8275
- setTimeout(() => {
8276
- newActiveCheats.forEach((id) => {
8277
- const cheat = cheats.find((c) => c.id === id);
8278
- if (cheat) {
8279
- nostalgist.applyCheat(cheat.code);
8280
- }
8281
- });
8282
- }, 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);
8283
8765
  };
8284
8766
  return {
8285
8767
  cheatsModalOpen,
8286
8768
  setCheatsModalOpen,
8287
8769
  activeCheats,
8288
- handleToggleCheat
8770
+ allCheats,
8771
+ // Unified list - replaces separate cheats + manualCheats
8772
+ handleToggleCheat,
8773
+ handleAddManualCheat
8289
8774
  };
8290
8775
  }
8291
8776
  function useGameRecording({
@@ -8462,10 +8947,13 @@ function useGamePlayer(props) {
8462
8947
  cheatsModalOpen,
8463
8948
  setCheatsModalOpen,
8464
8949
  activeCheats,
8465
- handleToggleCheat
8950
+ allCheats,
8951
+ handleToggleCheat,
8952
+ handleAddManualCheat
8466
8953
  } = useGameCheats({
8467
8954
  ...props,
8468
- nostalgist
8955
+ nostalgist,
8956
+ showToast
8469
8957
  });
8470
8958
  const {
8471
8959
  isRecording,
@@ -8521,7 +9009,9 @@ function useGamePlayer(props) {
8521
9009
  hardcoreRestrictions,
8522
9010
  // Cheats
8523
9011
  activeCheats,
9012
+ allCheats,
8524
9013
  handleToggleCheat,
9014
+ handleAddManualCheat,
8525
9015
  // Emulator
8526
9016
  nostalgist,
8527
9017
  volumeState,
@@ -8549,7 +9039,8 @@ var DEFAULT_SETTINGS = {
8549
9039
  muted: false,
8550
9040
  shader: "",
8551
9041
  showPerformanceOverlay: false,
8552
- showInputDisplay: false
9042
+ showInputDisplay: false,
9043
+ hapticsEnabled: true
8553
9044
  };
8554
9045
  function usePlayerPersistence(onSettingsChange) {
8555
9046
  const [settings, setSettings] = useState(DEFAULT_SETTINGS);
@@ -8656,7 +9147,9 @@ var es = {
8656
9147
  shortcuts: "Atajos",
8657
9148
  exit: "Salir",
8658
9149
  language: "Idioma",
8659
- selectLanguage: "Seleccionar idioma"
9150
+ selectLanguage: "Seleccionar idioma",
9151
+ haptics: "Vibraci\xF3n",
9152
+ enableHaptics: "Habilitar vibraci\xF3n"
8660
9153
  },
8661
9154
  overlay: {
8662
9155
  play: "JUGAR",
@@ -8739,7 +9232,10 @@ var es = {
8739
9232
  emptyDesc: "No se encontraron c\xF3digos para este juego",
8740
9233
  copy: "Copiar c\xF3digo",
8741
9234
  active: "{{count}} truco{{s}} activo(s)",
8742
- 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"
8743
9239
  },
8744
9240
  saveSlots: {
8745
9241
  title: "Guardar partida",
@@ -8884,7 +9380,9 @@ var fr = {
8884
9380
  shortcuts: "Raccourcis",
8885
9381
  exit: "Quitter",
8886
9382
  language: "Langue",
8887
- selectLanguage: "Choisir la langue"
9383
+ selectLanguage: "Choisir la langue",
9384
+ haptics: "Vibration",
9385
+ enableHaptics: "Activer la vibration"
8888
9386
  },
8889
9387
  overlay: {
8890
9388
  play: "JOUER",
@@ -8967,7 +9465,10 @@ var fr = {
8967
9465
  emptyDesc: "Aucun code trouv\xE9 pour ce jeu",
8968
9466
  copy: "Copier",
8969
9467
  active: "{{count}} actif(s)",
8970
- 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"
8971
9472
  },
8972
9473
  saveSlots: {
8973
9474
  title: "Sauvegardes",
@@ -9140,7 +9641,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9140
9641
  hardcoreRestrictions,
9141
9642
  // Cheats
9142
9643
  activeCheats,
9644
+ allCheats,
9143
9645
  handleToggleCheat,
9646
+ handleAddManualCheat,
9144
9647
  // Emulator Instance & State
9145
9648
  nostalgist,
9146
9649
  // Contains start, restart, etc.
@@ -9205,7 +9708,7 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9205
9708
  if (muted !== settings.muted) toggleMute();
9206
9709
  }
9207
9710
  }, [settingsLoaded]);
9208
- const { system, systemColor = "#00FF41", cheats = [], onExit } = props;
9711
+ const { system, systemColor = "#00FF41", onExit } = props;
9209
9712
  const handlePauseToggle = useCallback(() => {
9210
9713
  status === "ready" ? start() : togglePause();
9211
9714
  }, [status, start, togglePause]);
@@ -9261,6 +9764,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9261
9764
  const handleToggleInputDisplay = useCallback(() => {
9262
9765
  updateSettings({ showInputDisplay: !settings.showInputDisplay });
9263
9766
  }, [updateSettings, settings.showInputDisplay]);
9767
+ const handleToggleHaptics = useCallback(() => {
9768
+ updateSettings({ hapticsEnabled: !settings.hapticsEnabled });
9769
+ }, [updateSettings, settings.hapticsEnabled]);
9264
9770
  const handleToggleRecording = useCallback(async () => {
9265
9771
  if (!recordingSupported) {
9266
9772
  console.warn("[Recording] Not supported in this browser");
@@ -9347,7 +9853,12 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9347
9853
  system,
9348
9854
  isRunning: status === "running" || status === "paused",
9349
9855
  controls,
9350
- systemColor
9856
+ systemColor,
9857
+ hapticsEnabled: settings.hapticsEnabled,
9858
+ onButtonDown: nostalgist.pressDown,
9859
+ onButtonUp: nostalgist.pressUp,
9860
+ onPause: nostalgist.pause,
9861
+ onResume: nostalgist.resume
9351
9862
  }
9352
9863
  ),
9353
9864
  !isFullscreen2 && isMobile && /* @__PURE__ */ jsx(
@@ -9502,9 +10013,10 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9502
10013
  systemColor,
9503
10014
  cheatsModalOpen,
9504
10015
  setCheatsModalOpen,
9505
- cheats,
10016
+ cheats: allCheats,
9506
10017
  activeCheats,
9507
10018
  onToggleCheat: handleToggleCheat,
10019
+ onAddManualCheat: handleAddManualCheat,
9508
10020
  saveModalOpen,
9509
10021
  setSaveModalOpen,
9510
10022
  saveModalMode,
@@ -9524,7 +10036,9 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
9524
10036
  settingsModalOpen,
9525
10037
  setSettingsModalOpen,
9526
10038
  currentLanguage: props.currentLanguage,
9527
- onLanguageChange: props.onLanguageChange
10039
+ onLanguageChange: props.onLanguageChange,
10040
+ hapticsEnabled: settings.hapticsEnabled,
10041
+ onToggleHaptics: handleToggleHaptics
9528
10042
  }
9529
10043
  ),
9530
10044
  !isMobile && /* @__PURE__ */ jsx(