@strata-game-library/react-native-plugin 1.0.0

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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @strata/react-native-plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@strata/react-native-plugin.svg)](https://www.npmjs.com/package/@strata/react-native-plugin)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ React Native plugin for [Strata 3D](https://strata.game) - cross-platform input, device detection, and haptics for mobile games.
7
+
8
+ ## 📚 Documentation
9
+
10
+ **Full documentation is available at [strata.game/mobile/react-native](https://strata.game/mobile/react-native/)**
11
+
12
+ ---
13
+
14
+ ## 🏢 Enterprise Context
15
+
16
+ **Strata** is the Games & Procedural division of the [jbcom enterprise](https://jbcom.github.io). This plugin is part of a coherent suite of specialized tools, sharing a unified design system and interconnected with sibling organizations like [Agentic](https://agentic.dev) and [Extended Data](https://extendeddata.dev).
17
+
18
+ ## Features
19
+
20
+ - **Device Detection** - Identify device type, platform, and performance capabilities
21
+ - **Input Handling** - Unified touch input handling with `StrataInputProvider`
22
+ - **Haptic Feedback** - Cross-platform vibration with intensity control
23
+ - **Safe Area Insets** - Native safe area detection for notches
24
+ - **Orientation** - Get and set screen orientation
25
+ - **Performance Mode** - Detect low power mode and hardware levels
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @strata/react-native-plugin
31
+ cd ios && pod install
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```tsx
37
+ import { useDevice, useInput, useHaptics } from '@strata/react-native-plugin';
38
+
39
+ function Game() {
40
+ const device = useDevice();
41
+ const input = useInput();
42
+ const { trigger } = useHaptics();
43
+
44
+ return (
45
+ <View>
46
+ <Text>Platform: {device.platform}</Text>
47
+ <Button onPress={() => trigger({ intensity: 'medium' })} />
48
+ </View>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Related
54
+
55
+ - [Strata Documentation](https://strata.game) - Full documentation
56
+ - [Strata Core](https://github.com/strata-game-library/core) - Main library
57
+ - [Capacitor Plugin](https://github.com/strata-game-library/capacitor-plugin) - Capacitor version
58
+
59
+ ## License
60
+
61
+ MIT © [Jon Bogaty](https://github.com/jbcom)
@@ -0,0 +1,66 @@
1
+ buildscript {
2
+ ext.safeExtGet = {prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath "com.android.tools.build:gradle:8.1.1"
11
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10"
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "kotlin-android"
17
+
18
+ def getExtOrDefault(name) {
19
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["StrataReactNativePlugin_" + name]
20
+ }
21
+
22
+ android {
23
+ namespace "com.jbcom.strata"
24
+ compileSdkVersion getExtOrDefault("compileSdkVersion") ?: 34
25
+
26
+ defaultConfig {
27
+ minSdkVersion getExtOrDefault("minSdkVersion") ?: 21
28
+ targetSdkVersion getExtOrDefault("targetSdkVersion") ?: 34
29
+ }
30
+
31
+ buildTypes {
32
+ release {
33
+ minifyEnabled false
34
+ }
35
+ }
36
+
37
+ lintOptions {
38
+ disable "GradleCompatible"
39
+ }
40
+
41
+ compileOptions {
42
+ sourceCompatibility JavaVersion.VERSION_1_8
43
+ targetCompatibility JavaVersion.VERSION_1_8
44
+ }
45
+
46
+ kotlinOptions {
47
+ jvmTarget = "1.8"
48
+ }
49
+ }
50
+
51
+ repositories {
52
+ mavenLocal()
53
+ maven {
54
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
55
+ url("$projectDir/../node_modules/react-native/android")
56
+ }
57
+ google()
58
+ mavenCentral()
59
+ }
60
+
61
+ dependencies {
62
+ //noinspection GradleDynamicVersion
63
+ implementation "com.facebook.react:react-android:+"
64
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
65
+ implementation "androidx.core:core-ktx:1.9.0"
66
+ }
@@ -0,0 +1,6 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.jbcom.strata">
3
+
4
+ <uses-permission android:name="android.permission.VIBRATE" />
5
+
6
+ </manifest>
@@ -0,0 +1,353 @@
1
+ package com.jbcom.strata
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.content.pm.ActivityInfo
6
+ import android.os.Build
7
+ import android.os.VibrationEffect
8
+ import android.os.Vibrator
9
+ import android.os.VibratorManager
10
+ import android.os.PowerManager
11
+ import android.app.ActivityManager
12
+ import android.view.InputDevice
13
+ import android.view.WindowManager
14
+ import android.view.KeyEvent
15
+ import android.view.MotionEvent
16
+ import android.view.View
17
+ import androidx.core.view.ViewCompat
18
+ import androidx.core.view.WindowInsetsCompat
19
+ import com.facebook.react.bridge.*
20
+ import com.facebook.react.modules.core.DeviceEventManagerModule
21
+
22
+ class StrataModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
23
+
24
+ private var isListening = false
25
+ private val buttons = Arguments.createMap()
26
+ private val leftStick = Arguments.createMap()
27
+ private val rightStick = Arguments.createMap()
28
+ private val triggers = Arguments.createMap()
29
+
30
+ init {
31
+ reactContext.addLifecycleEventListener(this)
32
+ resetGamepadState()
33
+ }
34
+
35
+ private fun resetGamepadState() {
36
+ leftStick.putDouble("x", 0.0)
37
+ leftStick.putDouble("y", 0.0)
38
+ rightStick.putDouble("x", 0.0)
39
+ rightStick.putDouble("y", 0.0)
40
+ triggers.putDouble("left", 0.0)
41
+ triggers.putDouble("right", 0.0)
42
+
43
+ // Initialize buttons (common ones)
44
+ val buttonList = listOf("a", "b", "x", "y", "l1", "r1", "l2", "r2", "start", "select", "dpadUp", "dpadDown", "dpadLeft", "dpadRight")
45
+ for (button in buttonList) {
46
+ buttons.putBoolean(button, false)
47
+ }
48
+ }
49
+
50
+ override fun getName(): String {
51
+ return "StrataReactNativePlugin"
52
+ }
53
+
54
+ override fun getConstants(): Map<String, Any>? {
55
+ val constants = mutableMapOf<String, Any>()
56
+ constants["platform"] = "android"
57
+ return constants
58
+ }
59
+
60
+ override fun onHostResume() {
61
+ startListening()
62
+ }
63
+
64
+ override fun onHostPause() {
65
+ stopListening()
66
+ }
67
+
68
+ override fun onHostDestroy() {
69
+ stopListening()
70
+ }
71
+
72
+ private fun startListening() {
73
+ if (isListening) return
74
+ isListening = true
75
+ }
76
+
77
+ private fun stopListening() {
78
+ isListening = false
79
+ }
80
+
81
+ @ReactMethod
82
+ fun getDeviceProfile(promise: Promise) {
83
+ try {
84
+ val context = reactApplicationContext
85
+ val metrics = context.resources.displayMetrics
86
+ val configuration = context.resources.configuration
87
+
88
+ val profile = Arguments.createMap()
89
+
90
+ // Device Type
91
+ val deviceType = when {
92
+ isFoldable() -> "foldable"
93
+ (configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE -> "tablet"
94
+ else -> "mobile"
95
+ }
96
+ profile.putString("deviceType", deviceType)
97
+ profile.putString("platform", "android")
98
+
99
+ // Input Mode (Default to touch, check for gamepads later)
100
+ profile.putString("inputMode", if (hasGamepadConnected()) "gamepad" else "touch")
101
+
102
+ // Orientation
103
+ val orientation = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) "landscape" else "portrait"
104
+ profile.putString("orientation", orientation)
105
+
106
+ profile.putBoolean("hasTouch", context.packageManager.hasSystemFeature("android.hardware.touchscreen"))
107
+ profile.putBoolean("hasGamepad", hasGamepadConnected())
108
+
109
+ profile.putDouble("screenWidth", metrics.widthPixels.toDouble() / metrics.density)
110
+ profile.putDouble("screenHeight", metrics.heightPixels.toDouble() / metrics.density)
111
+ profile.putDouble("pixelRatio", metrics.density.toDouble())
112
+
113
+ // Safe Area Insets
114
+ profile.putMap("safeAreaInsets", getSafeAreaInsetsMap())
115
+
116
+ promise.resolve(profile)
117
+ } catch (e: Exception) {
118
+ promise.reject("ERR_DEVICE_PROFILE", e.message)
119
+ }
120
+ }
121
+
122
+ // Compatibility method for getDeviceInfo
123
+ @ReactMethod
124
+ fun getDeviceInfo(promise: Promise) {
125
+ getDeviceProfile(promise)
126
+ }
127
+
128
+ @ReactMethod
129
+ fun triggerHaptics(options: ReadableMap, promise: Promise) {
130
+ val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
131
+ val vibratorManager = reactApplicationContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
132
+ vibratorManager.defaultVibrator
133
+ } else {
134
+ @Suppress("DEPRECATION")
135
+ reactApplicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
136
+ }
137
+
138
+ if (!vibrator.hasVibrator()) {
139
+ promise.resolve(false)
140
+ return
141
+ }
142
+
143
+ val duration = if (options.hasKey("duration")) options.getInt("duration").toLong() else 50L
144
+ val intensity = if (options.hasKey("customIntensity")) (options.getDouble("customIntensity") * 255).toInt() else 128
145
+
146
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
147
+ vibrator.vibrate(VibrationEffect.createOneShot(duration, intensity.coerceIn(1, 255)))
148
+ } else {
149
+ @Suppress("DEPRECATION")
150
+ vibrator.vibrate(duration)
151
+ }
152
+ promise.resolve(true)
153
+ }
154
+
155
+ // Compatibility method for triggerHaptic(intensity)
156
+ @ReactMethod
157
+ fun triggerHaptic(intensity: String, promise: Promise) {
158
+ val options = Arguments.createMap()
159
+ options.putString("intensity", intensity)
160
+ when (intensity) {
161
+ "light" -> options.putDouble("customIntensity", 0.3)
162
+ "medium" -> options.putDouble("customIntensity", 0.6)
163
+ "heavy" -> options.putDouble("customIntensity", 1.0)
164
+ }
165
+ triggerHaptics(options, promise)
166
+ }
167
+
168
+ @ReactMethod
169
+ @Synchronized
170
+ fun getInputSnapshot(promise: Promise) {
171
+ val snapshot = Arguments.createMap()
172
+ snapshot.putDouble("timestamp", System.currentTimeMillis().toDouble())
173
+
174
+ val snapshotButtons = Arguments.createMap()
175
+ val buttonsIter = buttons.keySetIterator()
176
+ while (buttonsIter.hasNextKey()) {
177
+ val key = buttonsIter.nextKey()
178
+ snapshotButtons.putBoolean(key, buttons.getBoolean(key))
179
+ }
180
+ snapshot.putMap("buttons", snapshotButtons)
181
+
182
+ val snapshotLeftStick = Arguments.createMap()
183
+ snapshotLeftStick.putDouble("x", leftStick.getDouble("x"))
184
+ snapshotLeftStick.putDouble("y", leftStick.getDouble("y"))
185
+ snapshot.putMap("leftStick", snapshotLeftStick)
186
+
187
+ val snapshotRightStick = Arguments.createMap()
188
+ snapshotRightStick.putDouble("x", rightStick.getDouble("x"))
189
+ snapshotRightStick.putDouble("y", rightStick.getDouble("y"))
190
+ snapshot.putMap("rightStick", snapshotRightStick)
191
+
192
+ val snapshotTriggers = Arguments.createMap()
193
+ snapshotTriggers.putDouble("left", triggers.getDouble("left"))
194
+ snapshotTriggers.putDouble("right", triggers.getDouble("right"))
195
+ snapshot.putMap("triggers", snapshotTriggers)
196
+
197
+ val gamepads = Arguments.createArray()
198
+ val deviceIds = InputDevice.getDeviceIds()
199
+ for (deviceId in deviceIds) {
200
+ val device = InputDevice.getDevice(deviceId)
201
+ if (device != null && (device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
202
+ device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK)) {
203
+ gamepads.pushInt(deviceId)
204
+ }
205
+ }
206
+
207
+ snapshot.putArray("connectedGamepads", gamepads)
208
+ promise.resolve(snapshot)
209
+ }
210
+
211
+ @ReactMethod
212
+ fun setOrientation(orientation: String) {
213
+ val activity = currentActivity ?: return
214
+ val orientationConstant = when (orientation) {
215
+ "portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
216
+ "landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
217
+ else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
218
+ }
219
+ activity.requestedOrientation = orientationConstant
220
+ }
221
+
222
+ @ReactMethod
223
+ fun getPerformanceMode(promise: Promise) {
224
+ val map = Arguments.createMap()
225
+ val context = reactApplicationContext
226
+
227
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
228
+ val isLowPowerMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
229
+ powerManager?.isPowerSaveMode ?: false
230
+ } else false
231
+
232
+ val mi = ActivityManager.MemoryInfo()
233
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
234
+ activityManager?.getMemoryInfo(mi)
235
+
236
+ val mode = when {
237
+ isLowPowerMode || mi.totalMem < 2L * 1024 * 1024 * 1024 -> "low"
238
+ mi.totalMem < 4L * 1024 * 1024 * 1024 -> "medium"
239
+ else -> "high"
240
+ }
241
+
242
+ map.putString("mode", mode)
243
+ map.putBoolean("isLowPowerMode", isLowPowerMode)
244
+ map.putDouble("totalMemory", mi.totalMem.toDouble())
245
+ promise.resolve(map)
246
+ }
247
+
248
+ @ReactMethod
249
+ fun getSafeAreaInsets(promise: Promise) {
250
+ promise.resolve(getSafeAreaInsetsMap())
251
+ }
252
+
253
+ // Methods to be called from MainActivity to update state
254
+ @Synchronized
255
+ fun handleKeyEvent(event: KeyEvent) {
256
+ val isDown = event.action == KeyEvent.ACTION_DOWN
257
+ val button = when (event.keyCode) {
258
+ KeyEvent.KEYCODE_BUTTON_A -> "a"
259
+ KeyEvent.KEYCODE_BUTTON_B -> "b"
260
+ KeyEvent.KEYCODE_BUTTON_X -> "x"
261
+ KeyEvent.KEYCODE_BUTTON_Y -> "y"
262
+ KeyEvent.KEYCODE_BUTTON_L1 -> "l1"
263
+ KeyEvent.KEYCODE_BUTTON_R1 -> "r1"
264
+ KeyEvent.KEYCODE_BUTTON_L2 -> "l2"
265
+ KeyEvent.KEYCODE_BUTTON_R2 -> "r2"
266
+ KeyEvent.KEYCODE_BUTTON_START -> "start"
267
+ KeyEvent.KEYCODE_BUTTON_SELECT -> "select"
268
+ KeyEvent.KEYCODE_DPAD_UP -> "dpadUp"
269
+ KeyEvent.KEYCODE_DPAD_DOWN -> "dpadDown"
270
+ KeyEvent.KEYCODE_DPAD_LEFT -> "dpadLeft"
271
+ KeyEvent.KEYCODE_DPAD_RIGHT -> "dpadRight"
272
+ else -> null
273
+ }
274
+ button?.let { buttons.putBoolean(it, isDown) }
275
+ }
276
+
277
+ @Synchronized
278
+ fun handleMotionEvent(event: MotionEvent) {
279
+ if (event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK &&
280
+ event.action == MotionEvent.ACTION_MOVE) {
281
+
282
+ leftStick.putDouble("x", event.getAxisValue(MotionEvent.AXIS_X).toDouble())
283
+ leftStick.putDouble("y", event.getAxisValue(MotionEvent.AXIS_Y).toDouble())
284
+
285
+ rightStick.putDouble("x", event.getAxisValue(MotionEvent.AXIS_Z).toDouble())
286
+ rightStick.putDouble("y", event.getAxisValue(MotionEvent.AXIS_RZ).toDouble())
287
+
288
+ triggers.putDouble("left", event.getAxisValue(MotionEvent.AXIS_BRAKE).toDouble())
289
+ triggers.putDouble("right", event.getAxisValue(MotionEvent.AXIS_GAS).toDouble())
290
+ }
291
+ }
292
+
293
+ private fun isFoldable(): Boolean {
294
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
295
+ return reactApplicationContext.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle")
296
+ }
297
+ return false
298
+ }
299
+
300
+ private fun hasGamepadConnected(): Boolean {
301
+ val deviceIds = InputDevice.getDeviceIds()
302
+ for (deviceId in deviceIds) {
303
+ val device = InputDevice.getDevice(deviceId)
304
+ if (device != null) {
305
+ val sources = device.sources
306
+ if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
307
+ sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
308
+ return true
309
+ }
310
+ }
311
+ }
312
+ return false
313
+ }
314
+
315
+ private fun getSafeAreaInsetsMap(): ReadableMap {
316
+ val insetsMap = Arguments.createMap()
317
+ val density = reactApplicationContext.resources.displayMetrics.density
318
+ val activity = reactApplicationContext.currentActivity
319
+
320
+ var insetsApplied = false
321
+
322
+ if (activity != null) {
323
+ val windowInsets = ViewCompat.getRootWindowInsets(activity.window.decorView)
324
+ if (windowInsets != null) {
325
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
326
+ insetsMap.putDouble("top", cutoutInsets.top.toDouble() / density)
327
+ insetsMap.putDouble("right", cutoutInsets.right.toDouble() / density)
328
+ insetsMap.putDouble("bottom", cutoutInsets.bottom.toDouble() / density)
329
+ insetsMap.putDouble("left", cutoutInsets.left.toDouble() / density)
330
+ insetsApplied = true
331
+ }
332
+ }
333
+
334
+ if (!insetsApplied || insetsMap.getDouble("top") == 0.0) {
335
+ val resourceId = reactApplicationContext.resources.getIdentifier("status_bar_height", "dimen", "android")
336
+ val statusBarHeight = if (resourceId > 0) {
337
+ reactApplicationContext.resources.getDimensionPixelSize(resourceId).toDouble() / density
338
+ } else {
339
+ 0.0
340
+ }
341
+ if (!insetsApplied) {
342
+ insetsMap.putDouble("top", statusBarHeight)
343
+ insetsMap.putDouble("right", 0.0)
344
+ insetsMap.putDouble("bottom", 0.0)
345
+ insetsMap.putDouble("left", 0.0)
346
+ } else if (insetsMap.getDouble("top") == 0.0) {
347
+ insetsMap.putDouble("top", statusBarHeight)
348
+ }
349
+ }
350
+
351
+ return insetsMap
352
+ }
353
+ }
@@ -0,0 +1,16 @@
1
+ package com.jbcom.strata
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class StrataPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(StrataModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
@@ -0,0 +1,91 @@
1
+ import type React from 'react';
2
+ export interface DeviceProfile {
3
+ deviceType: 'mobile' | 'tablet' | 'foldable' | 'desktop';
4
+ platform: 'ios' | 'android' | 'web';
5
+ inputMode: 'touch' | 'keyboard' | 'gamepad' | 'hybrid';
6
+ orientation: 'portrait' | 'landscape';
7
+ hasTouch: boolean;
8
+ hasGamepad: boolean;
9
+ screenWidth: number;
10
+ screenHeight: number;
11
+ pixelRatio: number;
12
+ safeAreaInsets: {
13
+ top: number;
14
+ right: number;
15
+ bottom: number;
16
+ left: number;
17
+ };
18
+ performanceMode: 'low' | 'medium' | 'high';
19
+ }
20
+ export interface TouchState {
21
+ identifier: string;
22
+ pageX: number;
23
+ pageY: number;
24
+ locationX: number;
25
+ locationY: number;
26
+ timestamp: number;
27
+ }
28
+ export interface InputSnapshot {
29
+ timestamp: number;
30
+ leftStick: {
31
+ x: number;
32
+ y: number;
33
+ };
34
+ rightStick: {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ buttons: Record<string, boolean>;
39
+ triggers: {
40
+ left: number;
41
+ right: number;
42
+ };
43
+ touches: TouchState[];
44
+ }
45
+ export interface HapticsOptions {
46
+ type?: 'impact' | 'notification' | 'selection';
47
+ intensity?: 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error';
48
+ customIntensity?: number;
49
+ duration?: number;
50
+ pattern?: number[];
51
+ }
52
+ /**
53
+ * Hook to get and track device information
54
+ */
55
+ export declare function useDevice(): DeviceProfile;
56
+ /**
57
+ * Hook to handle input state
58
+ */
59
+ export declare function useInput(): InputSnapshot;
60
+ /**
61
+ * A component that captures touch gestures and provides them to the game
62
+ */
63
+ export declare const StrataInputProvider: React.FC<{
64
+ children: React.ReactNode;
65
+ onInput?: (snapshot: InputSnapshot) => void;
66
+ }>;
67
+ /**
68
+ * Hook for haptic feedback
69
+ */
70
+ export declare function useHaptics(): {
71
+ trigger: (options: HapticsOptions) => Promise<void>;
72
+ };
73
+ /**
74
+ * Set screen orientation
75
+ */
76
+ export declare function setOrientation(_orientation: 'portrait' | 'landscape' | 'default'): Promise<void>;
77
+ /**
78
+ * Hook for control hints based on device and input
79
+ */
80
+ export declare function useControlHints(): {
81
+ movement: string;
82
+ action: string;
83
+ camera: string;
84
+ };
85
+ /**
86
+ * Subscribe to gamepad connection events
87
+ */
88
+ export declare function onGamepadUpdate(callback: (event: {
89
+ connected: boolean;
90
+ }) => void): import("react-native").EmitterSubscription | null;
91
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { NativeModules, NativeEventEmitter, Platform, Dimensions, PixelRatio, View } from 'react-native';
3
+ const { RNStrata } = NativeModules;
4
+ if (!RNStrata && Platform.OS === 'ios') {
5
+ console.warn('RNStrata: Native module not found. Check your native installation.');
6
+ }
7
+ const eventEmitter = RNStrata ? new NativeEventEmitter(RNStrata) : null;
8
+ /**
9
+ * Hook to get and track device information
10
+ */
11
+ export function useDevice() {
12
+ const [deviceProfile, setDeviceProfile] = useState(() => {
13
+ const { width, height } = Dimensions.get('window');
14
+ return {
15
+ deviceType: 'mobile',
16
+ platform: Platform.OS,
17
+ inputMode: 'touch',
18
+ orientation: height >= width ? 'portrait' : 'landscape',
19
+ hasTouch: true,
20
+ hasGamepad: false,
21
+ screenWidth: width,
22
+ screenHeight: height,
23
+ pixelRatio: PixelRatio.get(),
24
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
25
+ performanceMode: 'high',
26
+ };
27
+ });
28
+ useEffect(() => {
29
+ const updateDeviceInfo = async () => {
30
+ if (Platform.OS === 'ios' && RNStrata) {
31
+ try {
32
+ const details = await RNStrata.getDeviceDetails();
33
+ setDeviceProfile({
34
+ ...details,
35
+ performanceMode: 'high', // Default for now as not in RNStrata yet
36
+ });
37
+ }
38
+ catch (e) {
39
+ console.error('Failed to get native device details', e);
40
+ }
41
+ }
42
+ else {
43
+ const { width, height } = Dimensions.get('window');
44
+ setDeviceProfile(prev => ({
45
+ ...prev,
46
+ screenWidth: width,
47
+ screenHeight: height,
48
+ orientation: height >= width ? 'portrait' : 'landscape',
49
+ }));
50
+ }
51
+ };
52
+ updateDeviceInfo();
53
+ const subscription = Dimensions.addEventListener('change', updateDeviceInfo);
54
+ return () => {
55
+ if (subscription?.remove) {
56
+ subscription.remove();
57
+ }
58
+ };
59
+ }, []);
60
+ return deviceProfile;
61
+ }
62
+ /**
63
+ * Hook to handle input state
64
+ */
65
+ export function useInput() {
66
+ const [input, setInput] = useState({
67
+ timestamp: Date.now(),
68
+ leftStick: { x: 0, y: 0 },
69
+ rightStick: { x: 0, y: 0 },
70
+ buttons: {},
71
+ triggers: { left: 0, right: 0 },
72
+ touches: [],
73
+ });
74
+ useEffect(() => {
75
+ let interval;
76
+ if (Platform.OS === 'ios' && RNStrata) {
77
+ interval = setInterval(async () => {
78
+ try {
79
+ const snapshot = await RNStrata.getGamepadSnapshot();
80
+ if (snapshot) {
81
+ setInput(prev => ({
82
+ ...snapshot,
83
+ touches: prev.touches, // Preserve touches from provider
84
+ }));
85
+ }
86
+ }
87
+ catch (_e) {
88
+ // Ignore gamepad errors in poll
89
+ }
90
+ }, 16); // ~60fps
91
+ }
92
+ return () => {
93
+ if (interval)
94
+ clearInterval(interval);
95
+ };
96
+ }, []);
97
+ return input;
98
+ }
99
+ /**
100
+ * A component that captures touch gestures and provides them to the game
101
+ */
102
+ export const StrataInputProvider = ({ children, onInput }) => {
103
+ const touches = useRef(new Map());
104
+ const updateTouches = (event) => {
105
+ const changedTouches = event.nativeEvent.changedTouches;
106
+ const timestamp = event.nativeEvent.timestamp;
107
+ changedTouches.forEach(touch => {
108
+ touches.current.set(touch.identifier, {
109
+ identifier: touch.identifier,
110
+ pageX: touch.pageX,
111
+ pageY: touch.pageY,
112
+ locationX: touch.locationX,
113
+ locationY: touch.locationY,
114
+ timestamp,
115
+ });
116
+ });
117
+ const snapshot = {
118
+ timestamp,
119
+ leftStick: { x: 0, y: 0 },
120
+ rightStick: { x: 0, y: 0 },
121
+ buttons: {},
122
+ triggers: { left: 0, right: 0 },
123
+ touches: Array.from(touches.current.values()),
124
+ };
125
+ onInput?.(snapshot);
126
+ };
127
+ const removeTouches = (event) => {
128
+ const changedTouches = event.nativeEvent.changedTouches;
129
+ changedTouches.forEach(touch => {
130
+ touches.current.delete(touch.identifier);
131
+ });
132
+ const snapshot = {
133
+ timestamp: event.nativeEvent.timestamp,
134
+ leftStick: { x: 0, y: 0 },
135
+ rightStick: { x: 0, y: 0 },
136
+ buttons: {},
137
+ triggers: { left: 0, right: 0 },
138
+ touches: Array.from(touches.current.values()),
139
+ };
140
+ onInput?.(snapshot);
141
+ };
142
+ return (<View style={{ flex: 1 }} onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} onResponderGrant={updateTouches} onResponderMove={updateTouches} onResponderRelease={removeTouches} onResponderTerminate={removeTouches}>
143
+ {children}
144
+ </View>);
145
+ };
146
+ /**
147
+ * Hook for haptic feedback
148
+ */
149
+ export function useHaptics() {
150
+ const trigger = useCallback(async (options) => {
151
+ if (Platform.OS === 'web') {
152
+ if ('vibrate' in navigator) {
153
+ navigator.vibrate(options.duration || 50);
154
+ }
155
+ return;
156
+ }
157
+ if (Platform.OS === 'ios' && RNStrata) {
158
+ RNStrata.triggerHaptic(options.type || 'impact', options);
159
+ }
160
+ }, []);
161
+ return { trigger };
162
+ }
163
+ /**
164
+ * Set screen orientation
165
+ */
166
+ export async function setOrientation(_orientation) {
167
+ // Not implemented in RNStrata yet, but keeping the API
168
+ console.warn('setOrientation is not yet implemented in RNStrata');
169
+ }
170
+ /**
171
+ * Hook for control hints based on device and input
172
+ */
173
+ export function useControlHints() {
174
+ const { inputMode, hasGamepad } = useDevice();
175
+ if (hasGamepad || inputMode === 'gamepad') {
176
+ return {
177
+ movement: 'Left Stick',
178
+ action: 'Button A / X',
179
+ camera: 'Right Stick'
180
+ };
181
+ }
182
+ if (inputMode === 'touch') {
183
+ return {
184
+ movement: 'Virtual Joystick',
185
+ action: 'Tap Screen',
186
+ camera: 'Drag'
187
+ };
188
+ }
189
+ return {
190
+ movement: 'WASD / Left Stick',
191
+ action: 'Space / Button A',
192
+ camera: 'Mouse / Right Stick'
193
+ };
194
+ }
195
+ /**
196
+ * Subscribe to gamepad connection events
197
+ */
198
+ export function onGamepadUpdate(callback) {
199
+ if (eventEmitter) {
200
+ return eventEmitter.addListener('onGamepadUpdate', callback);
201
+ }
202
+ return null;
203
+ }
204
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/tests/index.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import { renderHook } from '@testing-library/react-hooks/native';
2
+ import { useDevice } from '../index';
3
+ import { NativeModules } from 'react-native';
4
+ jest.mock('react-native', () => ({
5
+ NativeModules: {
6
+ RNStrata: {
7
+ getDeviceDetails: jest.fn(),
8
+ getGamepadSnapshot: jest.fn(),
9
+ triggerHaptic: jest.fn(),
10
+ },
11
+ },
12
+ NativeEventEmitter: jest.fn(() => ({
13
+ addListener: jest.fn(),
14
+ removeListeners: jest.fn(),
15
+ })),
16
+ Platform: {
17
+ OS: 'ios',
18
+ select: jest.fn(obj => obj.ios),
19
+ },
20
+ Dimensions: {
21
+ get: jest.fn(() => ({ width: 390, height: 844 })),
22
+ addEventListener: jest.fn(() => ({ remove: jest.fn() })),
23
+ },
24
+ PixelRatio: {
25
+ get: jest.fn(() => 3),
26
+ },
27
+ }));
28
+ describe('useDevice', () => {
29
+ it('should return initial device profile', () => {
30
+ const mockDetails = {
31
+ deviceType: 'mobile',
32
+ platform: 'ios',
33
+ inputMode: 'touch',
34
+ orientation: 'portrait',
35
+ hasTouch: true,
36
+ hasGamepad: false,
37
+ screenWidth: 390,
38
+ screenHeight: 844,
39
+ pixelRatio: 3,
40
+ safeAreaInsets: { top: 47, right: 0, bottom: 34, left: 0 }
41
+ };
42
+ NativeModules.RNStrata.getDeviceDetails.mockResolvedValue(mockDetails);
43
+ const { result } = renderHook(() => useDevice());
44
+ // Initial state
45
+ expect(result.current.platform).toBe('ios');
46
+ expect(result.current.hasTouch).toBe(true);
47
+ });
48
+ });
49
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/tests/index.test.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/B,aAAa,EAAE;QACb,QAAQ,EAAE;YACR,gBAAgB,EAAE,IAAI,CAAC,EAAE,EAAE;YAC3B,kBAAkB,EAAE,IAAI,CAAC,EAAE,EAAE;YAC7B,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE;SACzB;KACF;IACD,kBAAkB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QACjC,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE;QACtB,eAAe,EAAE,IAAI,CAAC,EAAE,EAAE;KAC3B,CAAC,CAAC;IACH,QAAQ,EAAE;QACR,EAAE,EAAE,KAAK;QACT,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;KAChC;IACD,UAAU,EAAE;QACV,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACjD,gBAAgB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;KACzD;IACD,UAAU,EAAE;QACV,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;KACtB;CACF,CAAC,CAAC,CAAC;AAEJ,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,WAAW,GAAG;YAClB,UAAU,EAAE,QAAQ;YACpB,QAAQ,EAAE,KAAK;YACf,SAAS,EAAE,OAAO;YAClB,WAAW,EAAE,UAAU;YACvB,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,KAAK;YACjB,WAAW,EAAE,GAAG;YAChB,YAAY,EAAE,GAAG;YACjB,UAAU,EAAE,CAAC;YACb,cAAc,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;SAC3D,CAAC;QAED,aAAa,CAAC,QAAQ,CAAC,gBAA8B,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAEtF,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;QAEjD,gBAAgB;QAChB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+
4
+ @interface RCT_EXTERN_MODULE(RNStrata, RCTEventEmitter)
5
+
6
+ RCT_EXTERN_METHOD(getDeviceDetails:(RCTPromiseResolveBlock)resolve
7
+ rejecter:(RCTPromiseRejectBlock)reject)
8
+
9
+ RCT_EXTERN_METHOD(triggerHaptic:(NSString *)type
10
+ options:(NSDictionary *)options)
11
+
12
+ RCT_EXTERN_METHOD(getGamepadSnapshot:(RCTPromiseResolveBlock)resolve
13
+ rejecter:(RCTPromiseRejectBlock)reject)
14
+
15
+ @end
@@ -0,0 +1,239 @@
1
+ import Foundation
2
+ import UIKit
3
+ import GameController
4
+ import React
5
+
6
+ @objc(RNStrata)
7
+ class RNStrata: RCTEventEmitter {
8
+
9
+ private var hasListeners = false
10
+
11
+ override func supportedEvents() -> [String]! {
12
+ return ["onGamepadUpdate"]
13
+ }
14
+
15
+ override func startObserving() {
16
+ hasListeners = true
17
+ setupGamepadObservers()
18
+ }
19
+
20
+ override func stopObserving() {
21
+ hasListeners = false
22
+ NotificationCenter.default.removeObserver(self)
23
+ }
24
+
25
+ @objc override static func requiresMainQueueSetup() -> Bool {
26
+ return true
27
+ }
28
+
29
+ // MARK: - Device Detection
30
+
31
+ @objc(getDeviceDetails:rejecter:)
32
+ func getDeviceDetails(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
33
+ DispatchQueue.main.async {
34
+ do {
35
+ let device = UIDevice.current
36
+ let screen = UIScreen.main
37
+ let bounds = screen.bounds
38
+
39
+ var deviceType = "mobile"
40
+ if device.userInterfaceIdiom == .pad {
41
+ deviceType = "tablet"
42
+ } else if #available(iOS 14.0, *), device.userInterfaceIdiom == .mac {
43
+ deviceType = "desktop"
44
+ }
45
+ #if targetEnvironment(macCatalyst)
46
+ deviceType = "desktop"
47
+ #endif
48
+
49
+ let orientation: String
50
+ if let windowScene = self.getKeyWindow()?.windowScene {
51
+ switch windowScene.interfaceOrientation {
52
+ case .landscapeLeft, .landscapeRight:
53
+ orientation = "landscape"
54
+ default:
55
+ orientation = "portrait"
56
+ }
57
+ } else {
58
+ orientation = "portrait"
59
+ }
60
+
61
+ let window = self.getKeyWindow()
62
+ let safeArea = window?.safeAreaInsets ?? .zero
63
+
64
+ let hasGamepad = GCController.controllers().count > 0
65
+
66
+ let details: [String: Any] = [
67
+ "deviceType": deviceType,
68
+ "platform": "ios",
69
+ "inputMode": hasGamepad ? "gamepad" : "touch",
70
+ "orientation": orientation,
71
+ "hasTouch": true,
72
+ "hasGamepad": hasGamepad,
73
+ "screenWidth": bounds.width,
74
+ "screenHeight": bounds.height,
75
+ "pixelRatio": screen.scale,
76
+ "safeAreaInsets": [
77
+ "top": safeArea.top,
78
+ "right": safeArea.right,
79
+ "bottom": safeArea.bottom,
80
+ "left": safeArea.left
81
+ ]
82
+ ]
83
+
84
+ resolve(details)
85
+ } catch {
86
+ reject("DEVICE_ERROR", "Failed to get device details: \(error.localizedDescription)", error)
87
+ }
88
+ }
89
+ }
90
+
91
+ private func getKeyWindow() -> UIWindow? {
92
+ if #available(iOS 13.0, *) {
93
+ return UIApplication.shared.connectedScenes
94
+ .filter({$0.activationState == .foregroundActive})
95
+ .map({$0 as? UIWindowScene})
96
+ .compactMap({$0})
97
+ .first?.windows
98
+ .filter({$0.isKeyWindow}).first
99
+ } else {
100
+ return UIApplication.shared.keyWindow
101
+ }
102
+ }
103
+
104
+ // MARK: - Haptic Feedback
105
+
106
+ @objc(triggerHaptic:options:)
107
+ func triggerHaptic(type: String, options: [String: Any]) {
108
+ DispatchQueue.main.async {
109
+ switch type {
110
+ case "impact":
111
+ let styleStr = options["intensity"] as? String ?? "medium"
112
+ var style: UIImpactFeedbackGenerator.FeedbackStyle = .medium
113
+
114
+ switch styleStr {
115
+ case "light": style = .light
116
+ case "medium": style = .medium
117
+ case "heavy": style = .heavy
118
+ default: style = .medium
119
+ }
120
+
121
+ let generator = UIImpactFeedbackGenerator(style: style)
122
+ generator.prepare()
123
+ generator.impactOccurred()
124
+
125
+ case "notification":
126
+ let styleStr = options["intensity"] as? String ?? "success"
127
+ var style: UINotificationFeedbackGenerator.FeedbackType = .success
128
+
129
+ switch styleStr {
130
+ case "success": style = .success
131
+ case "warning": style = .warning
132
+ case "error": style = .error
133
+ default: style = .success
134
+ }
135
+
136
+ let generator = UINotificationFeedbackGenerator()
137
+ generator.prepare()
138
+ generator.notificationOccurred(style)
139
+
140
+ case "selection":
141
+ let generator = UISelectionFeedbackGenerator()
142
+ generator.prepare()
143
+ generator.selectionChanged()
144
+
145
+ default:
146
+ break
147
+ }
148
+ }
149
+ }
150
+
151
+ // MARK: - Gamepad Support
152
+
153
+ private func setupGamepadObservers() {
154
+ // Remove existing observers first to prevent duplicates
155
+ NotificationCenter.default.removeObserver(self, name: .GCControllerDidConnect, object: nil)
156
+ NotificationCenter.default.removeObserver(self, name: .GCControllerDidDisconnect, object: nil)
157
+
158
+ NotificationCenter.default.addObserver(self, selector: #selector(gamepadDidConnect), name: .GCControllerDidConnect, object: nil)
159
+ NotificationCenter.default.addObserver(self, selector: #selector(gamepadDidDisconnect), name: .GCControllerDidDisconnect, object: nil)
160
+ }
161
+
162
+ @objc private func gamepadDidConnect(notification: Notification) {
163
+ // Handle gamepad connection
164
+ if hasListeners {
165
+ sendEvent(withName: "onGamepadUpdate", body: ["connected": true])
166
+ }
167
+ }
168
+
169
+ @objc private func gamepadDidDisconnect(notification: Notification) {
170
+ // Handle gamepad disconnection
171
+ if hasListeners {
172
+ sendEvent(withName: "onGamepadUpdate", body: ["connected": false])
173
+ }
174
+ }
175
+
176
+ @objc(getGamepadSnapshot:rejecter:)
177
+ func getGamepadSnapshot(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
178
+ do {
179
+ let controllers = GCController.controllers()
180
+ guard let controller = controllers.first else {
181
+ resolve(nil)
182
+ return
183
+ }
184
+
185
+ var snapshot: [String: Any] = [:]
186
+
187
+ if let gamepad = controller.extendedGamepad {
188
+ snapshot["leftStick"] = ["x": gamepad.leftThumbstick.xAxis.value, "y": gamepad.leftThumbstick.yAxis.value]
189
+ snapshot["rightStick"] = ["x": gamepad.rightThumbstick.xAxis.value, "y": gamepad.rightThumbstick.yAxis.value]
190
+
191
+ var buttons: [String: Bool] = [
192
+ "a": gamepad.buttonA.isPressed,
193
+ "b": gamepad.buttonB.isPressed,
194
+ "x": gamepad.buttonX.isPressed,
195
+ "y": gamepad.buttonY.isPressed,
196
+ "leftShoulder": gamepad.leftShoulder.isPressed,
197
+ "rightShoulder": gamepad.rightShoulder.isPressed,
198
+ "leftTrigger": gamepad.leftTrigger.isPressed,
199
+ "rightTrigger": gamepad.rightTrigger.isPressed,
200
+ "dpadUp": gamepad.dpad.up.isPressed,
201
+ "dpadDown": gamepad.dpad.down.isPressed,
202
+ "dpadLeft": gamepad.dpad.left.isPressed,
203
+ "dpadRight": gamepad.dpad.right.isPressed
204
+ ]
205
+
206
+ if #available(iOS 12.1, *) {
207
+ buttons["leftStickButton"] = gamepad.leftThumbstickButton?.isPressed ?? false
208
+ buttons["rightStickButton"] = gamepad.rightThumbstickButton?.isPressed ?? false
209
+ }
210
+
211
+ if #available(iOS 13.0, *) {
212
+ buttons["menu"] = gamepad.buttonMenu.isPressed
213
+ buttons["options"] = gamepad.buttonOptions?.isPressed ?? false
214
+ }
215
+
216
+ if #available(iOS 14.0, *) {
217
+ buttons["home"] = gamepad.buttonHome?.isPressed ?? false
218
+ }
219
+
220
+ snapshot["buttons"] = buttons
221
+ snapshot["triggers"] = [
222
+ "left": gamepad.leftTrigger.value,
223
+ "right": gamepad.rightTrigger.value
224
+ ]
225
+ } else {
226
+ // Provide default values for simpler controllers to match InputSnapshot interface
227
+ snapshot["leftStick"] = ["x": 0, "y": 0]
228
+ snapshot["rightStick"] = ["x": 0, "y": 0]
229
+ snapshot["buttons"] = [:]
230
+ snapshot["triggers"] = ["left": 0, "right": 0]
231
+ }
232
+
233
+ snapshot["timestamp"] = Date().timeIntervalSince1970 * 1000
234
+ resolve(snapshot)
235
+ } catch {
236
+ reject("GAMEPAD_ERROR", "Failed to get gamepad snapshot: \(error.localizedDescription)", error)
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,19 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "RNStrata"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "12.0" }
14
+ s.source = { :git => "https://github.com/jbcom/nodejs-strata-react-native-plugin.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "RNStrata/**/*.{h,m,mm,swift}"
17
+
18
+ s.dependency "React-Core"
19
+ end
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@strata-game-library/react-native-plugin",
3
+ "version": "1.0.0",
4
+ "description": "React Native plugin for Strata 3D - cross-platform input, device detection, and haptics for mobile games",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "android",
18
+ "ios",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
24
+ "clean": "rimraf dist",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "jest",
27
+ "lint": "biome lint src/",
28
+ "lint:fix": "biome lint --write src/",
29
+ "format": "biome format --write src/"
30
+ },
31
+ "keywords": [
32
+ "react-native",
33
+ "strata",
34
+ "input",
35
+ "gamepad",
36
+ "touch",
37
+ "haptics",
38
+ "device-detection",
39
+ "cross-platform",
40
+ "mobile",
41
+ "ios",
42
+ "android"
43
+ ],
44
+ "author": "Jon Bogaty <jon@jonbogaty.com>",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/strata-game-library/react-native-plugin"
49
+ },
50
+ "homepage": "https://strata.game/react-native-plugin",
51
+ "bugs": {
52
+ "url": "https://github.com/strata-game-library/react-native-plugin/issues"
53
+ },
54
+ "peerDependencies": {
55
+ "@strata-game-library/core": ">=1.4.0",
56
+ "react": ">=18.0.0",
57
+ "react-native": ">=0.72.0"
58
+ },
59
+ "devDependencies": {
60
+ "@biomejs/biome": "^2.3.0",
61
+ "@react-native/babel-preset": "^0.83.1",
62
+ "@semantic-release/changelog": "^6.0.3",
63
+ "@semantic-release/git": "^10.0.1",
64
+ "@testing-library/react-hooks": "^8.0.1",
65
+ "@testing-library/react-native": "^13.3.3",
66
+ "@types/jest": "^30.0.0",
67
+ "@types/react": "^19.0.0",
68
+ "babel-jest": "^30.2.0",
69
+ "jest": "^30.2.0",
70
+ "react": "^19.2.3",
71
+ "react-native": "^0.83.1",
72
+ "react-test-renderer": "^19.2.3",
73
+ "rimraf": "^6.1.0",
74
+ "semantic-release": "^25.0.2",
75
+ "ts-jest": "^29.4.6",
76
+ "typescript": "^5.9.0"
77
+ },
78
+ "publishConfig": {
79
+ "access": "public"
80
+ }
81
+ }