@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 +61 -0
- package/android/build.gradle +66 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/jbcom/strata/StrataModule.kt +353 -0
- package/android/src/main/java/com/jbcom/strata/StrataPackage.kt +16 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/index.test.d.ts +2 -0
- package/dist/tests/index.test.d.ts.map +1 -0
- package/dist/tests/index.test.js +49 -0
- package/dist/tests/index.test.js.map +1 -0
- package/ios/RNStrata/RNStrata.m +15 -0
- package/ios/RNStrata/RNStrata.swift +239 -0
- package/ios/jbcom-strata-react-native-plugin.podspec +19 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @strata/react-native-plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@strata/react-native-plugin)
|
|
4
|
+
[](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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|