From 3c90998a6122742ce1c2969d909192ede16d11d3 Mon Sep 17 00:00:00 2001 From: Warren H Date: Wed, 17 Jun 2026 20:04:36 -0400 Subject: [PATCH] Initial I Want to Heal app --- .gitignore | 28 + DEPLOYMENT.md | 87 + admin.html | 13 + android/.gitignore | 101 + android/app/.gitignore | 2 + android/app/build.gradle | 54 + android/app/capacitor.build.gradle | 19 + android/app/proguard-rules.pro | 21 + .../myapp/ExampleInstrumentedTest.java | 26 + android/app/src/main/AndroidManifest.xml | 50 + .../iwanttoheal/ControllerBridgeActivity.java | 107 + .../warren/iwanttoheal/DualScreenPlugin.java | 315 + .../com/warren/iwanttoheal/GameActivity.java | 11 + .../com/warren/iwanttoheal/MainActivity.java | 11 + .../main/res/drawable-land-hdpi/splash.png | Bin 0 -> 7705 bytes .../main/res/drawable-land-mdpi/splash.png | Bin 0 -> 4040 bytes .../main/res/drawable-land-xhdpi/splash.png | Bin 0 -> 9251 bytes .../main/res/drawable-land-xxhdpi/splash.png | Bin 0 -> 13984 bytes .../main/res/drawable-land-xxxhdpi/splash.png | Bin 0 -> 17683 bytes .../main/res/drawable-port-hdpi/splash.png | Bin 0 -> 7934 bytes .../main/res/drawable-port-mdpi/splash.png | Bin 0 -> 4096 bytes .../main/res/drawable-port-xhdpi/splash.png | Bin 0 -> 9875 bytes .../main/res/drawable-port-xxhdpi/splash.png | Bin 0 -> 13346 bytes .../main/res/drawable-port-xxxhdpi/splash.png | Bin 0 -> 17489 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 + android/app/src/main/res/drawable/splash.png | Bin 0 -> 4040 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2786 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3450 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4341 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1869 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2110 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2725 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3981 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5036 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6593 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6644 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 9793 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10455 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9441 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 15529 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15916 bytes .../res/values/ic_launcher_background.xml | 4 + android/app/src/main/res/values/strings.xml | 7 + android/app/src/main/res/values/styles.xml | 22 + android/app/src/main/res/xml/file_paths.xml | 5 + .../getcapacitor/myapp/ExampleUnitTest.java | 18 + android/build.gradle | 29 + android/capacitor.settings.gradle | 3 + android/gradle.properties | 22 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + android/gradlew | 251 + android/gradlew.bat | 94 + android/settings.gradle | 5 + android/variables.gradle | 16 + capacitor.config.ts | 9 + .../bulldrome-1781641624302-2c45f7f6.png | Bin 0 -> 1229 bytes db/schema.sql | 290 + db/seed.sql | 1013 ++ eslint.config.js | 22 + index.html | 13 + package-lock.json | 3730 +++++++ package.json | 44 + public/boss-placeholder.svg | 10 + public/equipment-placeholder.svg | 9 + public/favicon.svg | 1 + public/icons.svg | 24 + scripts/backup-db.mjs | 20 + scripts/export-offline-profile.mjs | 18 + scripts/generate-service-worker.mjs | 63 + scripts/init-db.mjs | 262 + scripts/manage-ip-allowance.mjs | 78 + server/admin.mjs | 333 + server/game-api.d.mts | 3 + server/game-api.mjs | 2094 ++++ server/production.mjs | 77 + src/App.css | 4955 +++++++++ src/App.tsx | 795 ++ src/admin.tsx | 11 + src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/components/AdminScreen.tsx | 766 ++ src/components/AuthScreen.tsx | 193 + src/components/CombatScreen.tsx | 1414 +++ src/components/ControllerIcons.tsx | 102 + src/components/CustomizeScreen.tsx | 235 + src/components/EquipmentScreen.tsx | 537 + src/components/PvpRoguelikeScreen.tsx | 1220 +++ src/components/SettingsScreen.tsx | 245 + src/components/TalentScreen.tsx | 202 + src/dualScreen.tsx | 447 + src/game.ts | 157 + src/gameRepository.ts | 946 ++ src/index.css | 15 + src/input.tsx | 716 ++ src/main.tsx | 28 + src/nativeDualScreen.ts | 42 + src/offline-starter-profile.json | 9575 +++++++++++++++++ src/profile.ts | 387 + src/pvpRoguelike.ts | 46 + tsconfig.app.json | 25 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 16 + 109 files changed, 32775 insertions(+) create mode 100644 .gitignore create mode 100644 DEPLOYMENT.md create mode 100644 admin.html create mode 100644 android/.gitignore create mode 100644 android/app/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/capacitor.build.gradle create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java create mode 100644 android/app/src/main/java/com/warren/iwanttoheal/DualScreenPlugin.java create mode 100644 android/app/src/main/java/com/warren/iwanttoheal/GameActivity.java create mode 100644 android/app/src/main/java/com/warren/iwanttoheal/MainActivity.java create mode 100644 android/app/src/main/res/drawable-land-hdpi/splash.png create mode 100644 android/app/src/main/res/drawable-land-mdpi/splash.png create mode 100644 android/app/src/main/res/drawable-land-xhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-land-xxhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-land-xxxhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-port-hdpi/splash.png create mode 100644 android/app/src/main/res/drawable-port-mdpi/splash.png create mode 100644 android/app/src/main/res/drawable-port-xhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-port-xxhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-port-xxxhdpi/splash.png create mode 100644 android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/app/src/main/res/drawable/splash.png create mode 100644 android/app/src/main/res/layout/activity_main.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/xml/file_paths.xml create mode 100644 android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java create mode 100644 android/build.gradle create mode 100644 android/capacitor.settings.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle create mode 100644 android/variables.gradle create mode 100644 capacitor.config.ts create mode 100644 data/uploads/bosses/bulldrome-1781641624302-2c45f7f6.png create mode 100644 db/schema.sql create mode 100644 db/seed.sql create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/boss-placeholder.svg create mode 100644 public/equipment-placeholder.svg create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 scripts/backup-db.mjs create mode 100644 scripts/export-offline-profile.mjs create mode 100644 scripts/generate-service-worker.mjs create mode 100644 scripts/init-db.mjs create mode 100644 scripts/manage-ip-allowance.mjs create mode 100644 server/admin.mjs create mode 100644 server/game-api.d.mts create mode 100644 server/game-api.mjs create mode 100644 server/production.mjs create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/admin.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/components/AdminScreen.tsx create mode 100644 src/components/AuthScreen.tsx create mode 100644 src/components/CombatScreen.tsx create mode 100644 src/components/ControllerIcons.tsx create mode 100644 src/components/CustomizeScreen.tsx create mode 100644 src/components/EquipmentScreen.tsx create mode 100644 src/components/PvpRoguelikeScreen.tsx create mode 100644 src/components/SettingsScreen.tsx create mode 100644 src/components/TalentScreen.tsx create mode 100644 src/dualScreen.tsx create mode 100644 src/game.ts create mode 100644 src/gameRepository.ts create mode 100644 src/index.css create mode 100644 src/input.tsx create mode 100644 src/main.tsx create mode 100644 src/nativeDualScreen.ts create mode 100644 src/offline-starter-profile.json create mode 100644 src/profile.ts create mode 100644 src/pvpRoguelike.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80e4a07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +data/*.db +data/*.db-shm +data/*.db-wal +backups/*.db + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..d9c4e71 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,87 @@ +# Self-Hosting + +The game can run on your own Windows PC and use router port forwarding. Account +registration, sessions, characters, progression, inventory, loot history, and +leaderboards are stored in the SQLite database at `data/game.db`. + +That database file is the persistent storage. It remains after the server or PC +restarts. If the file is deleted or lost, all accounts and save data are lost, +so keep regular backups. + +## Build and run + +```powershell +npm ci +npm run db:init +npm run build +$env:HOST = "127.0.0.1" +$env:PORT = "4173" +$env:TRUST_PROXY = "1" +npm start +``` + +Run `npm run db:init` after pulling schema changes. It creates missing tables and +seed content without erasing existing player data. + +## Internet access + +Do not expose the Node port directly over unencrypted HTTP. Passwords would +travel across the internet without encryption. Put Caddy or another HTTPS +reverse proxy in front of the game: + +```caddyfile +game.example.com { + reverse_proxy 127.0.0.1:4173 +} +``` + +Point a domain or dynamic-DNS name to your public IP, forward router ports 80 +and 443 to the PC running Caddy, and allow those ports through Windows Firewall. +Caddy obtains and renews the HTTPS certificate. + +Keep the game server bound to `127.0.0.1`. Set `TRUST_PROXY=1` only when the +server can be reached solely through your local reverse proxy. This lets account +limits use the visitor's public IP instead of the proxy's address. + +## Account limits + +Registration permits one account per public IP by default. Login and API rate +limits also apply. To allow a shared household to create more accounts, run a +local administrator command: + +```powershell +npm run accounts:ip -- set 203.0.113.42 4 Smith-household +npm run accounts:ip -- list +npm run accounts:ip -- remove 203.0.113.42 +``` + +`set` changes the total number of accounts that address may create. `remove` +returns it to the default one-account limit. These commands must be run on the +host PC and are not exposed through the website. + +An IP account limit reduces casual account spam, but it is not DDoS protection. +Your router and internet connection can still be overwhelmed before requests +reach the game. Keep Windows patched, expose only required ports, and use router +or ISP protections where available. + +## Backups + +Create a consistent live backup with: + +```powershell +npm run db:backup +``` + +Backups are written to `backups/game-.db`. Copy them to another disk +or cloud backup location; backups left only on the game PC will not help if its +drive fails. Test restoration occasionally by stopping the server, preserving +the current `data/game.db`, and placing a backup at that path. + +## Current scaling boundary + +SQLite is a good fit for a small private server running one game process. Do not +run multiple Node server instances against this setup. + +Combat currently runs in the browser. It is suitable for a friend group, but a +modified client could submit false completion metrics. Move combat simulation +and run validation to the server before treating rankings as cheat-resistant. diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..ed4657c --- /dev/null +++ b/admin.html @@ -0,0 +1,13 @@ + + + + + + + Game Admin + + +
+ + + diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..6a7af21 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.warren.iwanttoheal" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.warren.iwanttoheal" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..488f8bd --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java b/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java new file mode 100644 index 0000000..1d95ac2 --- /dev/null +++ b/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java @@ -0,0 +1,107 @@ +package com.warren.iwanttoheal; + +import android.content.Intent; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import com.getcapacitor.BridgeActivity; + +public abstract class ControllerBridgeActivity extends BridgeActivity { + + public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + loadIntentUrl(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + loadIntentUrl(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) enableImmersiveMode(); + } + + private void loadIntentUrl() { + if (bridge == null || getIntent() == null) return; + String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL); + if (initialUrl == null || initialUrl.isEmpty()) return; + bridge.getWebView().post(() -> bridge.getWebView().loadUrl(initialUrl)); + } + + private void enableImmersiveMode() { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + ); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + String token = controllerToken(event.getKeyCode()); + if (token == null || bridge == null) return super.dispatchKeyEvent(event); + + if (event.getAction() == KeyEvent.ACTION_DOWN) { + boolean repeat = event.getRepeatCount() > 0; + String script = + "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," + + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; + bridge.getWebView().post( + () -> bridge.getWebView().evaluateJavascript(script, null) + ); + } + return true; + } + + private String controllerToken(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_A: + case KeyEvent.KEYCODE_ENTER: + return "Button0"; + case KeyEvent.KEYCODE_BUTTON_B: + case KeyEvent.KEYCODE_BACK: + return "Button1"; + case KeyEvent.KEYCODE_BUTTON_X: + return "Button2"; + case KeyEvent.KEYCODE_BUTTON_Y: + return "Button3"; + case KeyEvent.KEYCODE_BUTTON_L1: + return "Button4"; + case KeyEvent.KEYCODE_BUTTON_R1: + return "Button5"; + case KeyEvent.KEYCODE_BUTTON_L2: + return "Button6"; + case KeyEvent.KEYCODE_BUTTON_R2: + return "Button7"; + case KeyEvent.KEYCODE_BUTTON_SELECT: + return "Button8"; + case KeyEvent.KEYCODE_BUTTON_START: + return "Button9"; + case KeyEvent.KEYCODE_BUTTON_THUMBL: + return "Button10"; + case KeyEvent.KEYCODE_BUTTON_THUMBR: + return "Button11"; + case KeyEvent.KEYCODE_DPAD_UP: + return "Button12"; + case KeyEvent.KEYCODE_DPAD_DOWN: + return "Button13"; + case KeyEvent.KEYCODE_DPAD_LEFT: + return "Button14"; + case KeyEvent.KEYCODE_DPAD_RIGHT: + return "Button15"; + default: + return null; + } + } +} diff --git a/android/app/src/main/java/com/warren/iwanttoheal/DualScreenPlugin.java b/android/app/src/main/java/com/warren/iwanttoheal/DualScreenPlugin.java new file mode 100644 index 0000000..8fd2782 --- /dev/null +++ b/android/app/src/main/java/com/warren/iwanttoheal/DualScreenPlugin.java @@ -0,0 +1,315 @@ +package com.warren.iwanttoheal; + +import android.app.Presentation; +import android.content.Context; +import android.graphics.Color; +import android.hardware.display.DisplayManager; +import android.os.Bundle; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "DualScreen") +public class DualScreenPlugin extends Plugin { + + private TopDisplayPresentation presentation; + + @PluginMethod + public void getDisplays(PluginCall call) { + DisplayManager displayManager = + (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE); + Display currentDisplay = getActivity().getDisplay(); + JSArray displays = new JSArray(); + + for (Display display : displayManager.getDisplays()) { + displays.put(describeDisplay(display, currentDisplay)); + } + + JSObject result = new JSObject(); + result.put("currentDisplayId", currentDisplay == null ? -1 : currentDisplay.getDisplayId()); + result.put("displays", displays); + call.resolve(result); + } + + @PluginMethod + public void openTopDisplay(PluginCall call) { + getActivity().runOnUiThread(() -> { + try { + DisplayLaunchPlan launchPlan = createLaunchPlan(call.getInt("displayId")); + if (launchPlan.targetDisplay == null) { + call.reject("No secondary Android display is available."); + return; + } + + Display currentDisplay = getActivity().getDisplay(); + if ( + currentDisplay != null + && currentDisplay.getDisplayId() == launchPlan.targetDisplay.getDisplayId() + ) { + JSObject result = describeDisplay(currentDisplay, currentDisplay); + result.put("opened", true); + result.put("alreadyOnMainDisplay", true); + call.resolve(result); + return; + } + + String gameUrl = bridge.getLocalUrl(); + String controlsUrl = gameUrl + "/?display=bottom"; + String targetUrl = launchPlan.targetIsTopDisplay ? gameUrl : controlsUrl; + String currentUrl = launchPlan.currentIsTopDisplay ? gameUrl : controlsUrl; + + closePresentation(); + presentation = new TopDisplayPresentation( + getActivity(), + launchPlan.targetDisplay, + targetUrl + ); + presentation.setOnDismissListener(dialog -> { + presentation = null; + notifyListeners("displayDisconnected", new JSObject()); + }); + presentation.show(); + bridge.getWebView().loadUrl(currentUrl); + + JSObject result = describeDisplay(launchPlan.targetDisplay, getActivity().getDisplay()); + result.put("opened", true); + result.put("presentationMode", true); + result.put("targetIsTopDisplay", launchPlan.targetIsTopDisplay); + result.put("currentIsTopDisplay", launchPlan.currentIsTopDisplay); + call.resolve(result); + } catch (Exception exception) { + closePresentation(); + call.reject("Unable to open the upper display.", exception); + } + }); + } + + @PluginMethod + public void closeTopDisplay(PluginCall call) { + getActivity().runOnUiThread(() -> { + closePresentation(); + call.resolve(); + }); + } + + @Override + protected void handleOnDestroy() { + closePresentation(); + } + + private DisplayLaunchPlan createLaunchPlan(Integer requestedDisplayId) { + DisplayManager displayManager = + (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE); + Display currentDisplay = getActivity().getDisplay(); + int currentDisplayId = currentDisplay == null ? -1 : currentDisplay.getDisplayId(); + Display topDisplay = largestDisplay(displayManager.getDisplays()); + boolean currentIsTopDisplay = + topDisplay != null && topDisplay.getDisplayId() == currentDisplayId; + Display targetDisplay = null; + + if (requestedDisplayId != null) { + Display requested = displayManager.getDisplay(requestedDisplayId); + if ( + requested != null + && requested.getDisplayId() != currentDisplayId + && requested.getState() != Display.STATE_OFF + ) { + targetDisplay = requested; + } + } + + if (targetDisplay == null && currentIsTopDisplay) { + targetDisplay = smallestOtherDisplay(displayManager.getDisplays(), currentDisplayId); + } + + if (targetDisplay == null && topDisplay != null && topDisplay.getDisplayId() != currentDisplayId) { + targetDisplay = topDisplay; + } + + if (targetDisplay == null) { + targetDisplay = largestOtherDisplay( + displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION), + currentDisplayId + ); + } + + if (targetDisplay == null) { + targetDisplay = largestOtherDisplay(displayManager.getDisplays(), currentDisplayId); + } + + boolean targetIsTopDisplay = + topDisplay != null && targetDisplay != null + && targetDisplay.getDisplayId() == topDisplay.getDisplayId(); + return new DisplayLaunchPlan(targetDisplay, targetIsTopDisplay, currentIsTopDisplay); + } + + private Display largestDisplay(Display[] displays) { + Display selected = null; + long selectedPixels = -1; + for (Display display : displays) { + if (display.getState() == Display.STATE_OFF) continue; + long pixels = pixelCount(display); + if (pixels > selectedPixels) { + selected = display; + selectedPixels = pixels; + } + } + return selected; + } + + private Display largestOtherDisplay(Display[] displays, int currentDisplayId) { + Display selected = null; + long selectedPixels = -1; + for (Display display : displays) { + if ( + display.getDisplayId() == currentDisplayId + || display.getState() == Display.STATE_OFF + ) { + continue; + } + long pixels = pixelCount(display); + if (pixels > selectedPixels) { + selected = display; + selectedPixels = pixels; + } + } + return selected; + } + + private Display smallestOtherDisplay(Display[] displays, int currentDisplayId) { + Display selected = null; + long selectedPixels = Long.MAX_VALUE; + for (Display display : displays) { + if ( + display.getDisplayId() == currentDisplayId + || display.getState() == Display.STATE_OFF + ) { + continue; + } + long pixels = pixelCount(display); + if (pixels < selectedPixels) { + selected = display; + selectedPixels = pixels; + } + } + return selected; + } + + private long pixelCount(Display display) { + Display.Mode mode = display.getMode(); + return (long) mode.getPhysicalWidth() * mode.getPhysicalHeight(); + } + + private JSObject describeDisplay(Display display, Display currentDisplay) { + Display.Mode mode = display.getMode(); + JSObject result = new JSObject(); + result.put("id", display.getDisplayId()); + result.put("name", display.getName()); + result.put("width", mode.getPhysicalWidth()); + result.put("height", mode.getPhysicalHeight()); + result.put("refreshRate", mode.getRefreshRate()); + result.put( + "isCurrent", + currentDisplay != null && currentDisplay.getDisplayId() == display.getDisplayId() + ); + result.put( + "isPresentation", + (display.getFlags() & Display.FLAG_PRESENTATION) == Display.FLAG_PRESENTATION + ); + return result; + } + + private void closePresentation() { + if (presentation == null) return; + presentation.setOnDismissListener(null); + presentation.dismiss(); + presentation = null; + } + + private final class DisplayLaunchPlan { + + private final Display targetDisplay; + private final boolean targetIsTopDisplay; + private final boolean currentIsTopDisplay; + + DisplayLaunchPlan( + Display targetDisplay, + boolean targetIsTopDisplay, + boolean currentIsTopDisplay + ) { + this.targetDisplay = targetDisplay; + this.targetIsTopDisplay = targetIsTopDisplay; + this.currentIsTopDisplay = currentIsTopDisplay; + } + } + + private final class TopDisplayPresentation extends Presentation { + + private final String initialUrl; + + TopDisplayPresentation(Context context, Display display, String initialUrl) { + super(context, display); + this.initialUrl = initialUrl; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setBackgroundDrawableResource(android.R.color.black); + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + ); + + FrameLayout container = new FrameLayout(getContext()); + container.setBackgroundColor(Color.BLACK); + WebView webView = new WebView(getContext()); + configureWebView(webView); + container.addView( + webView, + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ); + setContentView(container); + webView.loadUrl(initialUrl); + } + } + + private void configureWebView(WebView webView) { + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setMediaPlaybackRequiresUserGesture(false); + settings.setAllowFileAccess(true); + settings.setAllowContentAccess(true); + webView.setBackgroundColor(Color.BLACK); + webView.setWebViewClient(new WebViewClient() { + @Override + public WebResourceResponse shouldInterceptRequest( + WebView view, + WebResourceRequest request + ) { + return bridge.getLocalServer().shouldInterceptRequest(request); + } + }); + } +} diff --git a/android/app/src/main/java/com/warren/iwanttoheal/GameActivity.java b/android/app/src/main/java/com/warren/iwanttoheal/GameActivity.java new file mode 100644 index 0000000..d6263df --- /dev/null +++ b/android/app/src/main/java/com/warren/iwanttoheal/GameActivity.java @@ -0,0 +1,11 @@ +package com.warren.iwanttoheal; + +import android.os.Bundle; + +public class GameActivity extends ControllerBridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(DualScreenPlugin.class); + super.onCreate(savedInstanceState); + } +} diff --git a/android/app/src/main/java/com/warren/iwanttoheal/MainActivity.java b/android/app/src/main/java/com/warren/iwanttoheal/MainActivity.java new file mode 100644 index 0000000..774494e --- /dev/null +++ b/android/app/src/main/java/com/warren/iwanttoheal/MainActivity.java @@ -0,0 +1,11 @@ +package com.warren.iwanttoheal; + +import android.os.Bundle; + +public class MainActivity extends ControllerBridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(DualScreenPlugin.class); + super.onCreate(savedInstanceState); + } +} diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e31573b4fc93e60d171f4046c0220e1463075d9e GIT binary patch literal 7705 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 wKmFJ0_JHhk1&0B>;%YXATM literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..807725501bdd92e94e51e7b2b0006f69e0083a0b GIT binary patch literal 9251 zcmeHMX;@R&){a`F6@fZ2$YhHaL=+Jr%uy6^0u)3B$1ZwbY4hL4)@C5Hq9nWtKai&>vt*`@mZjzr1xZ}*Z6 zvgY>gvv`p7;!Rzjr(o`O34vcjdYF{)$z!T*a&SycFz1b6e3rb*uPVY}wgGm=b~tQR z0Nz`60*}qnC&z)&r?-H|=k>tjKs>OVQy}2qc+ht7NazfF{q4hlko+SZe=hQ;)Bd5z zzqj;XMgGF#ekbx*{jn*s>6zaN|9iv!vhOy3{1^ZK`7EE_65ITjP5H}uH-G#)jDJuG z|EP&SkI8RN{%!OhBJ_6{|G=&P4b}L0{og?O&!M@ezrF)>>ndL*nYiLH97H8|Tw3jB zFMlW{H5{ok0*!s50Fs+bKsHfFl&Q541OEp;$5Q3ZSr6kbAZyjl!-I>v%UJmE4R>z$ zA?hIz0Ga_oVqK!^_C$xqMGaf++K7-Iw92R=GcZ`%_faH}<1)$@%nsFo4?N=?C-2rpCjJdVPqNUW@~ z_g6^xF!iK|(6-y5n^nV9ENtwtZPZ>&g*PVorB11{QoLO4971)DR^};j;vPDEy=h%8 zzhWtBNE9QmIfC6NyD1==u45_SQAIVJkxX9~lDm?)s8K&sI@GQwB`vPwg8>9#7-f=PxHYcTNWPNYWSk zFuJvYjOoka-V26p7IEuo%ao&m;hlIy5!?2KTTe|$;eeE{+q2ERUpYcrY@Rll0=Vnb0O|(;I&+pE-lJRTo1)k#EpJTQ${t7 zSX&Xn25)>?lA`eqvnAkwvhLo6MRE>-lHO)CpURpHh8ASd`F%yviicyFYuHM1bT={IV7Q)3x5nB-lIK#-LdxlL&z+mf2PxMD(UsH)5$>l!bqe1$|m zPevgJ+MV#em++j|hCSLR#c_G3dNYlPGYT_1u3h~ea+Vos=u*PWw-nYejK7*u2V-0( zwL=_JuqLDbF>N+~apFC)-Tt%Z8=`h2TaVBb*;A4fJ_i82YlW(XwB8RmX>73-a^|0b{ z=hClOdx#NKhrBQGakXqJW?|~`jB>b_FJ3qiE-GDa-U{@9_!?B>t+Uqbg3aWaO!pC zg*OZx*m+vdY^KIs2qz*}IbD6E3R0ZR8sO=BRcVlj)lPR1m{{Ub6%g7$?t)`nyK+T! zHlj@%ta{rlsO42E$8C=MBy{V?<-k>6KIR<=$wTy&3`u3YOu$8)afva7tH+FErsv=* z?~c<=Tcj|!gEmVhxZJ}kGH|QjOFlHHP8eTmGtUbXa_9-n31vgG?aI1yaR`Fa;ro~K z2CGAgu@u+2S@@G@m*5F`Vb)e|yI7Tyie;ClkCH%5HC)yd7CudLRjr+kOq5C*B2Vp`Ns`0P2 zxnNVQS=w)HRVR909HbL+tcRO0ug*zapMVC6;6g05-110VR>x%UzJ{n-Hh;Wa+DDXK zJ==s3ZW^J{RbNHQ6f71NPbHo)3g97%7R*LKyn~^0&8WG=b#kq+g|0bKSrh&X0Tym2 zn~78m((AsU54QZZc!t{o$5$#KQ3$zVF@@Zut}3*6dn0ie_JJbc>B zBll+H@@bg7gn3=EmzOnm>HVZ0XzL9iZWHST};m_&P@aYqiP6&d~{_5kuKF!#hr zU<14>hUnF9G-yx#`CKLlK2*6Nd3JQgMSm%(C#73QT*P0S;dd+bHfMY5O5-EPBFdGI zm^C{0V42yqt_DY&Bw_nEgja&8{*V<@y(>^MLd#J%>SzETkwOcdl@~kkvWiQZY^)Aq z{fA`~y$PqUvGmKT6NAujE%*`qdg`FzIa1RUrnnH3x?ys{TFw?kVK$3)F#zj%pkLz{GfNeJ%bhtoQx2)UbC^# z>owl!8xQn@_jPp+E@#L$`5s8(!rg9yLk9tcj;S4(ZkdyR-#{LrI}^VeUGd@W_aut< zJ_iO{=uH1~sL<|A<-(U!zVybYbe%hL#;nGo?P(s9AtEQ;c6JZ@g9yI~oI%HAu1bhOJx{W5DJn{DMY&<0W!r!kwC$KPtY3T4H?WI<+BW(+At|$L zwPiFyb|>8e(@6^PFGXi#sg95#xPmyKD3VYA^Uus%gYQiPwJ7}I_) z&fBh}AqQ1@U7z|-?#7(sb!Mzvg>PinlCk9mqk&iPg9DpM^&o5^;wG_HP`IFNr-wv6 zOCJmKtQ?Z7mXGA9tMJ0A4p|0f`pZm@hn_pTqSz@ceZ90pJavewOBxg2%#Mk$nxq`Gf?29dAFZw=i90v0-nG5BK%blDno5nRJ(s>d zEh2aI@%SmG0x5A4Jz<&9o(a1`&+2-QMB?uhX^q;eehR18r(`9L?sBaI6XGM%*L$Zj zG3RtDkZpccY-KW>s2LlT;;#cz&JdHE@Dt%HdbIA)GGk~?Ll3*ULWt#BT^m7OX9>~E z?`3JIS~vF~yVAQ})_9f#wm;!-N}NTJ?DbBCa4%rv$gG1`^LDy>lVFUTn@Jmk}U-8PN{wqZTBcfh8kWn5sXg$Hn||M zT?8ZmMsbh_>sgwAi|Nc}3^#O;<`+x!41P@9E>36O{^k2&a*-an)x&GKhCia zb)|9={g9IFva8SN^-Dj)N%RIwRWO!vDR9KyBYz9fAL?)DNfGo^U0O~LkR~YvU6`>$ z>baj#;i}8YmOw45n5_=M!z1?R%Ak24lq`c9XOt#xezf%*AbEtZrm9*|a;IDhmrlK) zMJ_U0J4!03l_RXpRo`KL>5*S6Oc**!>3L!J`7ytp$G}1QgAEMhk!L4G%WZs%ZDJIu zk&bR???>`21oUEBk3FiPzx#R2?m`>bB#aT&<@m7UV3={TD(fZtNqG4gw78#3!gkAh z-P-i|AOV7*D$17ZDTJz~KmBj;97ez0L!K6%L&Y3*teL%c0sFdF? zF4xw_p832UtE=YGIn${cw8CIi|HX=V0tL*1hAIUZOR_8PP9?C6q1T7ae$MrY=sNt- zFAmvGjB@$N#YTVq!M#v`6rpjNoj6}wC8SDZ=TZ}@3y@=$;`>ThJLqWYwS7KiI8r<* zU3y4LT3no}1qo;cs?kY7^4KD2$?$C9hW0l)Atq90yo+C+!%{{TLtV$pX7xY*Jv|tD zpprTYz`xO+cPL@FC*ob|_*?~y0b}G$>jz|2m#rQOm3-?3>3t~;n0Fvv;y9?dlat6s zNFD=UeJa1JX*u$RX@<*pjJJG?LSceN23sbR-@Is3Lxc)--u-c}2^2Cf114*fp*WaUUtkbZRQ z46{va@|Ji9pyf_YvIt~|{SJl}kP}HepmW-bY16S|nwSH}IA^j)OBcx~)d z^b3Mo^+th?`FdTdh#wc%Z|r7u?K4ux-~^3F7{8TfJ|iP_4;c8hfO?e`h&ORt{b zgvJ>TIw;}0u4fZ5nT<{4d6vYOJavDZ1SsH9>|%hjd1sx&5`11pcR*A*i$2jQfw!Kz zK9kywbX~a}9Re@DY%|-WUGlIBs!%#;ch^^VsA#P~SURj~RmCB54tEL1#+N(I>Z(Ad zhYh!Ek9S*eg(Rm_M;v`(8>`}q!k(NlRFRSg@9k+4qRbwa4BAil(zU;q!wo&u$7Z5U z<=BWlX&oIQ>#l+0S={wYG_S&CnavPBCr z3ji~OhTwN)-e*FKaaA)Co(5H0{71)3c8a<8AeL%7=k*nmY1*0V-<5Z`b@nl4Qbi^y z#r+!enrke7>;7tpraKZObsVF4a%D@|V^H+{t< za#CzZRX&6UW?V66S_?DWJbtXnjaF6LI5!&aKwc?*9}8QCF*KE`M942C&13WxBfa>Z4PA*eqPV6GMm9LQJP46**CXx$HT4 z@iNZ>(fK9nPQfub6Z&CB`IRCJ5UGkRy0!9=tBRF**jIoS z>QMBw6qtl0^nWDyr>+vMW;^l-yHLBP##4dD?H!_xkA<#%<6eFQoeh`noYfnTt_l#C z&Rclo`!C0?F~+Co`r17=Ib%`Mym|!( z*~@W8sFa3#@c6PajnXEx`i0zF40;@byxdvH@+jfWGD3C`Saa12FO(EE^(?Q(aAyc* zClu`r?u69m$e*U0VxA)%FrDgkU65F2@I)2DD0PqCCPSwsl(c~xTC7*1M4D|;^5F~;7FS|YQB=I-!TIF`X9ox0uAl} zp=>x$FpVi$-81%uIl4o_(jg-MY80(QsY=;i6b3X|XxYa6viS=KvV!gP9{!6MleqrM z;E9XBc6`+yFs_B(UA5AlAGCChO~ysn&fcp@8Lu*B8qR_NI>3(@J8v}76lP|_jr5@R zwi;swfhYi_AAYi}7Y!f_zRY{U$jzNlh%L3UjY}r9{HY&$ zmWrGhdmDoNY?8+tT7RWQsMTiM39O(w$asl`#XcHUZs<84WQr{*%8EAEiRCG3te;pV zP>zW7-)1QAz4V1h4N-?5H2q6_dsM#t7yc$DnEw5j_HXW0ey9s`9bSe6-d#IW`e;bA z>J$lo=mzW4#hj|#Yoh7xetZixn{>s(qzBAB`IEKPpm?|O z4e<7{3*+ph>plL)Atm?UwrwLd?5P|vL5DGWoDmiAt9iz8_ITE}hQ3~v&FJo`1|DJN zX^0c7VCZoXUj&IXlu_XlB;wtsK2eC*NJOeUOy@l0%%u!49&vf~UR^!&g}%O+k_l;N zoB0|lY6h^#@EZO;L;kem%4g%*BQnA zAn!6YUHpEWVLV#SSZ$LYZnNlf;9k7bE~-aCokCq+8I3M|JD_)0e6x1SKVrAq&>m{+ zEf?a7-1FxNygNk|J`;lW)J!u`S>%N_7-I-HnG4mA68Nv|PTDrERq2I-W?9Sy5sWca{uHO`+q{1}a;WO%lCWLM+I*Ae zy3L=*QksY_C03hxsts6b*7nglbY7xgI!dES{S8zK?)jE%LNF5QuWVAyw4M%+d|{k} zu5W7}gzrf#fC_g(MT5;~)R+8U{9fvQ425`0?T8RIDl|^Q5Po zF`<|TZZbjm1KmVihTpGXDN8i)ifL5>u)Latp{_A{g(ne!eepivVNO;efO#DAUBFy^ zI*a#?jF4xh=L9Try7jN854kT)r3n1bvZG-~$rebW?r2y70R2FFeRUv7!+M*)kv@#O zh|J6^cXN$qk+{8dL*eE|`}Y^005b)NjrliMpyHPBQRKJLUl0+u>;KC|>$d;@+dT29 zH0bZk-hYb3e?=Jo&$oo4qd@KfnDp1833P`)zW)DR?*EqYzm0%e`;W8yU17fmn7=FR rf2ZVsMTKqF%74gb8_I^%agb$tWlX#2_ijMygDzOwoW)q&`u2YSCS7pS literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..14c6c8fe39fcd51a0414866ad28cbe8ff3acb060 GIT binary patch literal 13984 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®q@N z14_H8ofW)l{|J8q+a)eH)I0r)>WXdzV%7J>PA~6_J)KLT90iYa^K=Wz7D!OybzqSru=f4?|KFl;Y)gP_H6V4x`~kZ6fE(xM1&;?72-TZNk+0 zr+Crr5yl%Iy@vfmt3eYFl!jIvPGFz^8Ek+2`48O1_pCX3xNWh-zBa{rIcc%+=|XVj zANYTg&s}TKb#OztQrCW(Xk?V^i{`q~%HtcveTxq(_HKeC9GzrtguMT4Nvs@KakPTA z9>*8bBZmLz`lK5=l)=b|=dT3a5ag^a1^znZyx5QKfUb1b9yacArRp%3@QWo(hrsCU z-K!-=jDmv!zb7XT>)r|-Z0Ry}lk2;dk-ECqMwr_nKN#x*X6~B5hVIN>6$1HwBz3Of z=Pk){AL5*=d90f17_qZEJLm;Q%WMdX=*N&!ki@E&cy7?>{1ssAH(tACtp*r@d^til z)x(1#6(kPD+joSF&J3sxJU@{-sWCS+pZq{Gsx=?z4wP;>?)1yHv0?X?VP{}cX4~aH zxeBPKw_rgW8rvewS1W2#^y+c>-183iMbJCqc38RN_o~__9-n|jcd&oA`m7*&Fqqpc z;Tev*0LS-ZK47Sq1unfvP1S43uA12P?PJmI8BeTYPr~R*tYUm^0;U%Hmu?bSZHEK6 zPjsW=E67Kq-&trmf;)UkmRABH2U)V)-eRT$j(%G12lLMsThSsU10iP#{)ZnvjzN$d z*K%P3`}oqyvpWP~venr>3viH8^`)Ma*=B31hw*Q+tqE>i2y7w!(o^lI^Yss^=tHW( z;cnCT(%B1gLz+TRGW9roFjI1EQTu-u`(f#RmZ8;FSN(bsC1J;+(i_R6mrW=yYx$cy z#%QKVrEx~kVMg~yo?^N28Wnk6x%L;J8i|*|ANEiNjq(Vhzuzl3ikpA*G!Z}kLAzAI z9qnySo%D|AuJj12%h;Otqjs(>LPj?rNdeU8so>P(C>XMzlho94ZD#w=cCOOU;=3&^ zsqAG!i{~lY271D|m>ztPV`)X@FO_;`wPjppYNQpM+ncvtz1lZjN>!Q^*I}T%uP78Z7tbV2$q3W_)14=kLFyJ z1GqL6T>ClgeZorL!}xP4f%OB_EsmJ`uw7dGWNV9OLlhb|UMpVhc{4@Bhh`tO!ZqzD zhusd<=K^ah!L@gQ?6dOpI-ge^e>S5W9eII57Zu16eU?GRbgKTeVk9yS{iK|O(zLR> zheb?;jwGCHS80NCn=jKxgJ>}qu4l%5NPihjzazGv#J?Jcyl;<#IW&x4mm>nrW8>}C z3U@aeD~)*F(0o^2{GnKVm$Jr#aZE ztl~TOkM^SdzJapQ((!-i8b!RkVQBKkL`2ZCBuy!qI1L{3Er526plVols~68U-^9Px zR(3{j;Z9RHX^muc0dUywJ|`yyZFf=k&-Gb#m4u73Lm5Ks%BfHj%2|gjn#i> zLC5pO$2Em9H;qoKQmMtl<@wgtPF1%2HariD5O~u>8=^*J&au~JH%Ih@&2Uging3U_ z0bzfKucW$ZHSx}!#buB?+-J)%RQbbXM-!BJTS&#dU_@lxU6>te2O+9 z@F{F{Nb!;{Cd`Gx+$G?11aB~S#wIH%D=*=7f7H@D@%B1)&bF$@t3JDq4l*%(wJTlh zo`?uMq{YilKUewPNaC)GuOr<8j9&ofqRU__BRUX^x8Cj3a;a$rXzgXqW>LR#CUn%~m)t zYC&ol(gAkbc^fd`xWU&bk5vT6KbFmsR=O78Bn%t7 znbw&=c+|T&#r+bls5rU6D#HMvqA<|;)BV%jOMonkm^p$7Vcel-Wwn$=uAJv&(8W>% z9))Fxpl*(%E#wFm_m!U~2HqgZs^2vaGeY(UfYKrSHV}w^D0N6!se5Ewy)Yy-!(2

aKj2hWG7>znxs|SE zN4rHtiSPqLskWp(?(_YYwgq+1@8v+~8As|(bC>$D(atG3ZE8-ZM3SVcg|vHQz$I=!(A`k`5= zOqR>&%G)$)k*QLz7MTB9wleWpv&N9Sta64wy}3Ytd?x!Ja8z>(z~(3UNFu^eFmn#6 zw!!gUxOuZi$PQIs*ixfZR3iLyADJ z5&s%tPfk>V!x|A-;oq%1!yk9H$UBP0ToA*EDtz(^!_AnF1bBQ7joj|? z5b)gSI8c8O$PYFE!vXJ<4gebg*9G9P2wcB{#kv0FItc5T@PDNo)}Rh4Us}L{e}xzW zhwt`)j`M)mP=G6H0;^&q=I0{jU%bIRkF#uLF;{vVC&H|_uc literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..244ca2506dbe0fd8f6a05520ac7d1a629ea81438 GIT binary patch literal 17683 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..bfabe6871a17a5e95b78fb30d49b7d2b4d2fe4c0 GIT binary patch literal 13346 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

VwO4Hu8X}%XHUAz_J@50rzbkIsat>4oWtQt< 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..6929071268eb03ee0f088142b6523566b78550e2 GIT binary patch literal 17489 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 KR&><$`~L^lv%S~= literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c023e50595074292c7361183a64de08cf9686c9c GIT binary patch literal 2786 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2127973b2d318df7085734d236d0ec649a2b0292 GIT binary patch literal 3450 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1e077104cd61e6a4c3707e87363b523077245a GIT binary patch literal 3981 zcmV;84|4E{P)?m z2Iqx7pAYgakSjnIq={jqe_xK2+h*EzWwd!kfWi5380F4z@bASS>p|K~(;2oMw*a3< zK?+G?T5chzM-ta%1;eyc>o{H=hukKL25q{GrPnd+0-KniwCBCEX}iOg#o7W)E)HXr zd(Ve|7lL$v@QH~D&KTpmX>rrCz0YZ)8#Xshc|FT^xTaguda8BDHIv&=Useyu_v$*1 zE3FHi#1s6Ccz|>ii^Xel9SMTXLZR>#xWSPCc*}KN2mWk^(?%1*8lH5juXvA@x zr?BV4&1|^Y#I)XED2#-ddR}jaHn0a$dUVOPmILihzZU<-5kl$t(?^2qT^lzN!aYAE zh>2~QChzL%dM+0NgnQN0)N}zfegGLpx|6}Bl2BR?!Pm-9642O<6%FqtvjQaqK`M+H9vOMH38sJ-;5jwj zl##{26!QLoq4Aere)&g;T^YKzue*<_n(J+&NC zv0cwQO6e?!4H7UsJY0P7#TS2!*yTaM+{VVnMM!|SiDA+JOR@=e0KTheJ7f}8J1Q=! z417T*5?s{cN37_asDgli@#mj^{wIiC?gZd4^B+oqgb-l}{&>t#HZqccZAJ}lRub@e zjF^#xB!owT@QQ>Ds5k*rJ%a|AVfYW21b+BMOjDhNv>>Zo67V?;!;+I&CjR0yA<$6 zK#3y+JtGk(%m@K861*4Jr(RqqfJ?Cn<@otY5i>>YL{+LerFJGsm@xvP-a-=iStDkl zfARmz4YHC*pxU&!5h6%{(O4M*8X6iGLC-D9BH^&tkOY1f60(AyM1m~BrKl$)0lXtj z2~}nUz>SMKAri)yK@UOG<@#1vNZ{d%=apU%Bw%}i|DKCsb{vP{vkndpRYnDQAV?x% zG>Yi$P`x7x2@VkXcBjn`4LOs+Tjr=4r2h7FaDgsnvI z(+sn!|NlCI|2E^dzQK4M$bCRU`{``p?;rHRzp;VwUxF;z#Q47tX9K_LZfF=>J0;Lj z3D%-7ycxj1Ya8RYLGQoq%_$OA012O#NkZEIv543R;sGXZOO(#L9qoS@3{)`ZS9?6)O zC$|WREgA{<4&7%=zUBkQ>!J78za3A4P)8kS7|AfB+W8%{vE(&hrjfA8CjklSI$hob zQB?_;n!|O;x?VwfjRFG(cazLoL4r*PV_ zyji-c5fC?lPA4J0UR;p1yB|Cs0tu@VNWi~88HSz@*Dc-bOA-mo0Q}besWz-KNT><{ z03b_Ry+94;qa;lB{FoqX7l5+reYrYXv)wn$Zivu>0RHdcx?eYfY)&Sj9z3F!KK0yD z)d>h(O_ugRY9!cx&)2QgIb0-R<@@xJ-~{0R0gU*~TR<99+ubM0BPMAA9o5kaqB_FBZTY*5{|3~`EnAXFK=4~m<+lP43BVCp zkx00xo6=;QY7q8TH{%cQ2HiVIi*C^{-?v?^j_N?A{I_g zRhG>#UzUG1l#q^A^?Kd}@Ygg0pgS0Uuo=O1N%@=BLE1=2BjIkS+d9CM^Gaz`szN=_ z*nEHr32*f5(#Kd zCSmU&BdeqPi0bH`ZGO*p+ArAt^#*LDIbXF1&PA}l!odw1p-xrXOG7``*?;oZCmKdx>NLce; zJ|s-8?E5UJpfLDCj*T+@gbE1+wXy3IorLxbB-jZ?SPx>PicQL6Af`%ymNJ1}LV`pc zsf>gyxg#m~PO=+nRoV^JG}B8(Pzj(+pd$$+brg`mh&BP!)JVeZk}N8wvX#KsWkXy*0>vhvI-=MFH^`z= zMgp!oYX7h6y^|{VGZKiolbhG_+mD6s@Lr!!0L3sHI4e$W~a}TJ2Jpl!XRkpTR)djZJ3*+|$&4AxruT`v&3>m9%6=eanyVsStv zBH<1I|1TebYuO3}v?i1AD17%=D=lq7oln@?@9@TBPOi(-y_J1^6^MiZ)S{)D3zDpM zI1J!F^D*Op+UFDSP(VPGgeSl#A3PJNDT0eii4*+vg5?Qm&l-UQvr@Sr%!XQu+0e;g zErEo?;O#v73F8m9`2>`Z5E=>Vz$hOY@!OjW*7j!$*6c`-Mct{BMnbS^!rP!RYvu!z zNI;$c41oVgn=eSh!x0it)(h}6M9fijWG8G_%|e1$yyZBvaYigef(Kr~2met3_mTY} z684pjg!N#QKOKqpYTO`!x4w;L6U6kUElDqE#8Y0x&j08H{p_6+4o)#Sde-58J?Mik zq=bY(9j*V!2cMG4CQok;xt_kKZ5*VL;GN9!S(ua;Lw9Gt%^wGFA3G3F0ut~Pe1C9+ z-Y#W&a}~%NU8p8hPI+;pM!Yblm#CvzOMt%v-Cq~<0qEpECy?;e$7hr4}XItd8=p^@_QNO9Afxb$)JiSh%hIK%9LADNo0_;EGp zhJuAxCcsEJdjUs+P)ARLQEoq*J(G$yVO>B1=8~Ga%U?JgM?xIPW2B~#s=U73?}2!GM-fMLwYkcVpd+cth{kB zy%e>zifhG&O$<3gqw8mUMvZwoeI9pMXmCr{1gc);HT{!OWQ3e%WBFt4yuey%8H0M z5@uvl9(T%9R*k869n|>*^vZC#pi2z~DUJc$8x1aHjyX!gjGPsLdRKPdz6$tM5)k~N zW~xWzDI^G0W)RPgn>r2NnVHj4gl=_N@{b;Zn zT^=Q&oFiXkB*dLdl;t?$w8gio=N(n$=;E}qqrI(2c#3W0MA@t&yd&E#Z^c>~39`kv zDo5=gHg<4$cdi6%-MaPr)YH=u98A+{x|v&a>y{jK&vI)Q?bj$gtE^4>Q9Hr$^$>`? z@WKl}&5eLpUww6lJavQOhY}4oA(@Qxhp92pe$XWbeq6&p!Ku zJP5#_-u#50{k$^h}~UKPw4IL6*uXFL7QJU^9W(jE#-) zrlzL9BW3}1bMheI!X-b;x7WtU%Phg%`g)BMn^|I*0JDm$*3RG3a*vG1xqfQ;FN;L|6*^H z6>KwD2_h|G`fx(>2W nm0GElTB(&OnKI&V600000NkvXXu0mjff+~0a literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..df0f15880bee46332dfc6622583215194f948b0f GIT binary patch literal 5036 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6cdf97c1196d48e9833487ff6de7c4cfc4e1232d GIT binary patch literal 6644 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2960cbb6104b915c84760f889deed9bff2b3e17a GIT binary patch literal 9793 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 y3vD~Wi9;mL_v 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..a40d73e9c68ea638c28c7b13e4bcd1e179a3a102 GIT binary patch literal 15916 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 + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c721bf --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + I want to Heal + I want to Heal + com.warren.iwanttoheal + com.warren.iwanttoheal + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle new file mode 100644 index 0000000..9a5fa87 --- /dev/null +++ b/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle new file mode 100644 index 0000000..b01e46f --- /dev/null +++ b/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 35 + targetSdkVersion = 35 + androidxActivityVersion = '1.10.1' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.16.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 0000000..442c490 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,9 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'com.warren.iwanttoheal', + appName: 'I want to Heal', + webDir: 'dist' +}; + +export default config; diff --git a/data/uploads/bosses/bulldrome-1781641624302-2c45f7f6.png b/data/uploads/bosses/bulldrome-1781641624302-2c45f7f6.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad58222ee421be365d613f42da6cb05d423fd02 GIT binary patch literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$farfn|!Pi(^Q|t+#jgW{WsV9RK)7 zcA8k1lgJg9ppK8NKZLG`IQlAyDRndn32Y3s6bL;0H8V#isD)YgikHEJ2K|Dq^?m8q zpB3!Be>n5z+?!hYAEDFt-Z@wIZN}`GcjwMpq|pUJSEO?Cc@JMu5^2z22xM5K#(1Vb zU*0Wo&-?rHZ<>3}TqocE>K3;H1Cu}l1Iq%hTi(o*r6z1|-19&G;g5?GRkt>0{gq~i zNjov@t^K-5+>Lp%^n_~;EAPiM&9t|)X(@_|%YpC;E@%F{=wJ8mFv3 z?YCpAFh9@~4zSu!48MN8tetX;@g%bf|BK!2-hn~QpZH>!AhJMr+cc=mFh4KvI&uAV zKDTGTKONQB>A1I|=6|@v&3jQ#zV-62fvFAARN(EIcI#k9gGoS9^-Vkf>+k<@23u)- zZnA*crp3CXR^jGJJ>Ci8K6wtg&ETK`yX7(CCckBI`%8C0E`2+-jy8Fe4bgrGCtBn-}gI^*)`m{g9eF;flD{vp5%h z`~7cJqW{moc9IQYXHfI2{dGc1O#-g@6}{tT4UV_C@r9Ee8hZh28}_YvBj4u9dy4A} zx7JtQl|lY>Rq~&1f9n^31=lVnk9#hgG6Sxe*b2;$@_EU;8j>Cr7<{!9X5L}$zt`SA z#q8(HxjeeOJdorC3>T^OMfL_~cb%AfG5}@}gUPw3O_x@`oh=!m|3y33NCcYH6d0DS zJbU%B!8zkr)`q>NJd0%u@}KbT>tbexdDfj_YwSw)LwjyVA3V|M`2OFW z2-{-aGe^#EgU9y8Pkw$?_s?4e%&156#ro8xD z|03@5!^5JKIpFg8>D%m=zDFp9lonZQ|1mM`7fhD+sF?{YrWib3{an^LB{Ts5;6mGM literal 0 HcmV?d00001 diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..d6f4a26 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,290 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS locations ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS dungeons ( + id INTEGER PRIMARY KEY, + location_id INTEGER NOT NULL REFERENCES locations(id), + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + recommended_level INTEGER NOT NULL DEFAULT 1, + content_type TEXT NOT NULL DEFAULT 'dungeon' CHECK (content_type IN ('dungeon', 'raid')), + party_size INTEGER NOT NULL DEFAULT 5, + completion_item_level INTEGER, + experience_reward INTEGER NOT NULL DEFAULT 100, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS difficulties ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + dropped_item_level INTEGER NOT NULL, + unlock_level INTEGER NOT NULL DEFAULT 1, + health_multiplier REAL NOT NULL DEFAULT 1, + damage_multiplier REAL NOT NULL DEFAULT 1, + experience_multiplier REAL NOT NULL DEFAULT 1, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS dungeon_difficulties ( + dungeon_id INTEGER NOT NULL REFERENCES dungeons(id) ON DELETE CASCADE, + difficulty_id INTEGER NOT NULL REFERENCES difficulties(id), + PRIMARY KEY (dungeon_id, difficulty_id) +); + +CREATE TABLE IF NOT EXISTS encounters ( + id INTEGER PRIMARY KEY, + dungeon_id INTEGER NOT NULL REFERENCES dungeons(id), + sequence INTEGER NOT NULL, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + encounter_type TEXT NOT NULL CHECK (encounter_type IN ('trash', 'boss')), + max_health INTEGER NOT NULL, + base_damage INTEGER NOT NULL, + tank_damage INTEGER NOT NULL, + party_damage INTEGER NOT NULL, + description TEXT NOT NULL, + image_url TEXT NOT NULL DEFAULT '/boss-placeholder.svg', + UNIQUE (dungeon_id, sequence) +); + +CREATE TABLE IF NOT EXISTS mechanics ( + id INTEGER PRIMARY KEY, + encounter_id INTEGER NOT NULL REFERENCES encounters(id) ON DELETE CASCADE, + name TEXT NOT NULL, + mechanic_type TEXT NOT NULL, + interval_seconds REAL, + power INTEGER NOT NULL DEFAULT 0, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + resource_name TEXT NOT NULL, + max_resource INTEGER NOT NULL DEFAULT 100, + theme_color TEXT NOT NULL DEFAULT '#e5b95f', + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS spells ( + id INTEGER PRIMARY KEY, + class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + spell_type TEXT NOT NULL, + resource_cost INTEGER NOT NULL, + cooldown_seconds REAL NOT NULL DEFAULT 0, + power INTEGER NOT NULL, + unlock_level INTEGER NOT NULL DEFAULT 1, + glyph TEXT NOT NULL DEFAULT '+', + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + slot TEXT NOT NULL CHECK (slot IN ('weapon', 'helmet', 'chest', 'gloves', 'boots', 'pants', 'ring', 'necklace', 'trinket', 'component')), + rarity TEXT NOT NULL, + item_level INTEGER NOT NULL, + healing_power INTEGER NOT NULL DEFAULT 0, + max_resource_bonus INTEGER NOT NULL DEFAULT 0, + glyph TEXT NOT NULL DEFAULT '?', + image_url TEXT NOT NULL DEFAULT '/equipment-placeholder.svg', + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS encounter_loot ( + encounter_id INTEGER NOT NULL REFERENCES encounters(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + difficulty_id INTEGER REFERENCES difficulties(id), + drop_weight INTEGER NOT NULL DEFAULT 100, + drop_chance REAL NOT NULL DEFAULT 0.65 CHECK (drop_chance BETWEEN 0 AND 1), + PRIMARY KEY (encounter_id, difficulty_id, item_id) +); + +CREATE TABLE IF NOT EXISTS dungeon_completion_loot ( + dungeon_id INTEGER NOT NULL REFERENCES dungeons(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + PRIMARY KEY (dungeon_id, item_id) +); + +CREATE TABLE IF NOT EXISTS crafting_recipes ( + id INTEGER PRIMARY KEY, + item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, + difficulty_id INTEGER REFERENCES difficulties(id), + source_dungeon_id INTEGER REFERENCES dungeons(id) ON DELETE CASCADE, + source_encounter_id INTEGER REFERENCES encounters(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS crafting_recipe_components ( + recipe_id INTEGER NOT NULL REFERENCES crafting_recipes(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + quantity INTEGER NOT NULL CHECK (quantity > 0), + PRIMARY KEY (recipe_id, item_id) +); + +CREATE TABLE IF NOT EXISTS item_sets ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS item_set_items ( + set_id INTEGER NOT NULL REFERENCES item_sets(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, + PRIMARY KEY (set_id, item_id) +); + +CREATE TABLE IF NOT EXISTS item_set_bonuses ( + id INTEGER PRIMARY KEY, + set_id INTEGER NOT NULL REFERENCES item_sets(id) ON DELETE CASCADE, + required_pieces INTEGER NOT NULL CHECK (required_pieces > 0), + effect_type TEXT NOT NULL, + description TEXT NOT NULL, + UNIQUE (set_id, required_pieces) +); + +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL UNIQUE COLLATE NOCASE, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + completed_dungeon_parts INTEGER NOT NULL DEFAULT 0, + completed_raid_phases INTEGER NOT NULL DEFAULT 0, + created_ip TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS account_ip_allowances ( + ip_address TEXT PRIMARY KEY, + max_accounts INTEGER NOT NULL CHECK (max_accounts >= 1), + note TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS characters ( + id INTEGER PRIMARY KEY, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + class_id INTEGER NOT NULL REFERENCES classes(id), + name TEXT NOT NULL, + level INTEGER NOT NULL DEFAULT 1, + experience INTEGER NOT NULL DEFAULT 0, + talent_points INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (account_id, class_id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY, + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + active_character_id INTEGER REFERENCES characters(id), + expires_at TEXT NOT NULL, + created_ip TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS sessions_token_hash_idx ON sessions(token_hash); +CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at); + +CREATE TABLE IF NOT EXISTS character_ability_slots ( + character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + slot_number INTEGER NOT NULL CHECK (slot_number BETWEEN 1 AND 6), + spell_id INTEGER REFERENCES spells(id), + PRIMARY KEY (character_id, slot_number), + UNIQUE (character_id, spell_id) +); + +CREATE TABLE IF NOT EXISTS talents ( + id INTEGER PRIMARY KEY, + class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + max_rank INTEGER NOT NULL DEFAULT 1, + tier INTEGER NOT NULL DEFAULT 1, + branch INTEGER NOT NULL DEFAULT 1, + prerequisite_talent_id INTEGER REFERENCES talents(id), + prerequisite_rank INTEGER NOT NULL DEFAULT 0, + effect_type TEXT NOT NULL DEFAULT 'placeholder', + effect_value_per_rank REAL NOT NULL DEFAULT 0, + glyph TEXT NOT NULL DEFAULT '+', + description TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS character_talents ( + character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + talent_id INTEGER NOT NULL REFERENCES talents(id) ON DELETE CASCADE, + rank INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (character_id, talent_id) +); + +CREATE TABLE IF NOT EXISTS level_progression ( + level INTEGER PRIMARY KEY CHECK (level BETWEEN 1 AND 25), + experience_required INTEGER NOT NULL, + talent_points_total INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS game_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS character_inventory ( + character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id), + quantity INTEGER NOT NULL DEFAULT 1, + equipped INTEGER NOT NULL DEFAULT 0 CHECK (equipped IN (0, 1)), + PRIMARY KEY (character_id, item_id) +); + +CREATE TABLE IF NOT EXISTS encounter_loot_rolls ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + run_token TEXT NOT NULL, + encounter_id INTEGER NOT NULL REFERENCES encounters(id), + difficulty_id INTEGER NOT NULL REFERENCES difficulties(id), + item_id INTEGER REFERENCES items(id), + dropped INTEGER NOT NULL CHECK (dropped IN (0, 1)), + was_duplicate INTEGER NOT NULL DEFAULT 0 CHECK (was_duplicate IN (0, 1)), + quantity_after INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (character_id, run_token, encounter_id, difficulty_id) +); + +CREATE TABLE IF NOT EXISTS encounter_loot_roll_items ( + roll_id INTEGER NOT NULL REFERENCES encounter_loot_rolls(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id), + quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0), + was_duplicate INTEGER NOT NULL DEFAULT 0 CHECK (was_duplicate IN (0, 1)), + quantity_after INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (roll_id, item_id) +); + +CREATE TABLE IF NOT EXISTS dungeon_runs ( + id INTEGER PRIMARY KEY, + character_id INTEGER NOT NULL REFERENCES characters(id), + dungeon_id INTEGER NOT NULL REFERENCES dungeons(id), + difficulty_id INTEGER REFERENCES difficulties(id), + result TEXT NOT NULL CHECK (result IN ('active', 'victory', 'defeat', 'abandoned')), + character_name TEXT NOT NULL DEFAULT '', + class_name TEXT NOT NULL DEFAULT '', + character_level INTEGER NOT NULL DEFAULT 1, + average_item_level REAL NOT NULL DEFAULT 1, + resource_spent INTEGER NOT NULL DEFAULT 0, + duration_seconds INTEGER NOT NULL DEFAULT 0, + leaderboard_eligible INTEGER NOT NULL DEFAULT 0 CHECK (leaderboard_eligible IN (0, 1)), + start_part INTEGER NOT NULL DEFAULT 1, + completed_parts INTEGER NOT NULL DEFAULT 1, + started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + completed_at TEXT +); diff --git a/db/seed.sql b/db/seed.sql new file mode 100644 index 0000000..dda95b2 --- /dev/null +++ b/db/seed.sql @@ -0,0 +1,1013 @@ +INSERT OR IGNORE INTO locations (id, slug, name, description) VALUES + (1, 'ember-wastes', 'The Ember Wastes', 'A scorched frontier surrounding a buried citadel.'), + (2, 'crown-caldera', 'The Crown Caldera', 'A volcanic basin surrounding the stronghold of the Ember Crown.'); + +INSERT OR IGNORE INTO dungeons + (id, location_id, slug, name, recommended_level, content_type, party_size, completion_item_level, experience_reward, description) +VALUES + (1, 1, 'ashen-halls', 'The Ashen Halls', 1, 'dungeon', 5, NULL, 125, 'Break the cinder cult before the old furnace awakens.'), + (2, 2, 'citadel-of-the-ember-crown', 'Citadel of the Ember Crown', 1, 'raid', 10, 10, 175, 'Lead ten allies through the caldera and break the Ember Crown across three phases.'); + +UPDATE dungeons +SET slug = 'bulldrome-hunting-ground', + name = 'Bulldrome Hunting Ground', + location_id = 1, + recommended_level = 1, + content_type = 'dungeon', + party_size = 5, + completion_item_level = NULL, + experience_reward = 125, + description = 'A three-boss hunt featuring Bulldrome, Yian Kut-Ku, and Rathian.' +WHERE id = 1; +UPDATE dungeons SET party_size = 10, completion_item_level = NULL, experience_reward = 175 +WHERE slug = 'citadel-of-the-ember-crown'; + +INSERT OR IGNORE INTO difficulties + (id, slug, name, dropped_item_level, unlock_level, health_multiplier, damage_multiplier, experience_multiplier, description) +VALUES + (1, 'initiate', 'Initiate', 5, 1, 1.0, 1.0, 1.0, 'Entry-level dungeon difficulty.'), + (2, 'veteran', 'Veteran', 10, 5, 1.35, 1.2, 1.5, 'Enemies deal more damage and drop stronger gear.'), + (3, 'champion', 'Champion', 15, 10, 1.7, 1.45, 2.2, 'Demanding encounters for developed characters.'), + (4, 'mythic', 'Mythic', 20, 15, 2.1, 1.75, 3.0, 'Endgame dungeon difficulty.'), + (5, 'ascendant', 'Ascendant', 25, 20, 2.6, 2.1, 4.0, 'The current pinnacle difficulty.'), + (101, 'raid-normal', 'Normal', 7, 1, 1.0, 1.0, 1.25, 'The opening raid difficulty, tuned for a ten-player party.'); + +UPDATE difficulties SET + dropped_item_level = CASE slug + WHEN 'raid-normal' THEN 10 ELSE dropped_item_level END, + unlock_level = CASE slug + WHEN 'initiate' THEN 1 WHEN 'veteran' THEN 5 WHEN 'champion' THEN 10 + WHEN 'mythic' THEN 15 WHEN 'ascendant' THEN 20 ELSE unlock_level END, + health_multiplier = CASE slug + WHEN 'initiate' THEN 1.0 WHEN 'veteran' THEN 1.35 WHEN 'champion' THEN 1.7 + WHEN 'mythic' THEN 2.1 WHEN 'ascendant' THEN 2.6 ELSE health_multiplier END, + damage_multiplier = CASE slug + WHEN 'initiate' THEN 1.0 WHEN 'veteran' THEN 1.2 WHEN 'champion' THEN 1.45 + WHEN 'mythic' THEN 1.75 WHEN 'ascendant' THEN 2.1 ELSE damage_multiplier END, + experience_multiplier = CASE slug + WHEN 'initiate' THEN 1.0 WHEN 'veteran' THEN 1.5 WHEN 'champion' THEN 2.2 + WHEN 'mythic' THEN 3.0 WHEN 'ascendant' THEN 4.0 ELSE experience_multiplier END; + +INSERT OR IGNORE INTO dungeon_difficulties (dungeon_id, difficulty_id) VALUES + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 101); + +INSERT OR IGNORE INTO encounters + (id, dungeon_id, sequence, slug, name, encounter_type, max_health, base_damage, tank_damage, party_damage, description) +VALUES + (1, 1, 1, 'ashfang-pack', 'Ashfang Pack', 'trash', 390, 13, 7, 24, 'Three beasts snap at random party members.'), + (2, 1, 2, 'cinder-adepts', 'Cinder Adepts', 'trash', 470, 16, 10, 25, 'Cultists pressure the tank and throw embers into the group.'), + (3, 1, 3, 'bulldrome', 'Bulldrome', 'boss', 820, 18, 13, 27, 'A charging wyvern tests the party with heavy tusk strikes.'), + (10, 1, 4, 'sootborn-guard', 'Sootborn Guard', 'trash', 420, 14, 8, 24, 'A hulking guard forged from living soot.'), + (11, 1, 5, 'ember-channelers', 'Ember Channelers', 'trash', 500, 17, 11, 25, 'Cultists channelling raw ember energy.'), + (12, 1, 6, 'yian-kut-ku', 'Yian Kut-Ku', 'boss', 880, 20, 14, 28, 'A frantic bird wyvern scatters flame and wingbeats through the party.'), + (20, 1, 7, 'cinder-colossus', 'Cinder Colossus', 'trash', 460, 15, 9, 25, 'An enormous construct of slag and anger.'), + (21, 1, 8, 'flame-wardens', 'Flame Wardens', 'trash', 530, 18, 12, 26, 'Elite guards of the furnace sanctum.'), + (22, 1, 9, 'rathian', 'Rathian', 'boss', 950, 22, 16, 30, 'The queen of the land punishes mistakes with poison and crushing sweeps.'), + (100, 2, 1, 'scorched-vanguard', 'Scorched Vanguard', 'trash', 1050, 17, 10, 48, 'The citadel vanguard splits its assault across the raid.'), + (101, 2, 2, 'pyrebinders', 'Pyrebinders', 'trash', 1220, 19, 12, 50, 'Battle-mages bind living flame to random allies.'), + (102, 2, 3, 'gatekeeper-arkon', 'Gatekeeper Arkon', 'boss', 2150, 22, 15, 53, 'Arkon seals the caldera gate behind waves of burning force.'), + (103, 2, 4, 'molten-behemoths', 'Molten Behemoths', 'trash', 1160, 18, 11, 49, 'Two molten beasts batter both tanks and trample the raid.'), + (104, 2, 5, 'crown-zealots', 'Crown Zealots', 'trash', 1320, 20, 13, 51, 'Fanatics ignite themselves to empower their allies.'), + (105, 2, 6, 'high-inquisitor-vael', 'High Inquisitor Vael', 'boss', 2320, 24, 16, 55, 'Vael brands the unworthy and calls down columns of fire.'), + (106, 2, 7, 'emberwing-brood', 'Emberwing Brood', 'trash', 1260, 19, 12, 51, 'A flock of emberwings dives through the raid in sequence.'), + (107, 2, 8, 'crownforged-sentinels', 'Crownforged Sentinels', 'trash', 1420, 21, 14, 53, 'Ancient sentinels awaken to defend the crown chamber.'), + (108, 2, 9, 'the-ember-crown', 'The Ember Crown', 'boss', 2550, 26, 18, 57, 'The living crown burns above its throne, demanding perfect coordination.'); + +UPDATE encounters +SET slug = 'bulldrome', + name = 'Bulldrome', + description = 'A charging wyvern tests the party with heavy tusk strikes.', + image_url = COALESCE(NULLIF(image_url, ''), '/boss-placeholder.svg') +WHERE id = 3; + +UPDATE encounters +SET slug = 'yian-kut-ku', + name = 'Yian Kut-Ku', + description = 'A frantic bird wyvern scatters flame and wingbeats through the party.', + image_url = COALESCE(NULLIF(image_url, ''), '/boss-placeholder.svg') +WHERE id = 12; + +UPDATE encounters +SET slug = 'rathian', + name = 'Rathian', + description = 'The queen of the land punishes mistakes with poison and crushing sweeps.', + image_url = COALESCE(NULLIF(image_url, ''), '/boss-placeholder.svg') +WHERE id = 22; + +INSERT OR IGNORE INTO mechanics + (id, encounter_id, name, mechanic_type, interval_seconds, power, description) +VALUES + (1, 3, 'Cinder Pulse', 'party_damage', 4.9, 12, 'Deals damage to the full party.'), + (2, 3, 'Searing Mark', 'dispellable_dot', 7.7, 7, 'Marks one target with recurring damage until cleansed.'), + (10, 12, 'Ember Wave', 'party_damage', 5.6, 14, 'A wave of ember energy hits the entire party.'), + (11, 12, 'Brand of Sacrifice', 'dispellable_dot', 8.4, 9, 'Brands a target with burning sigils.'), + (20, 22, 'Furnace Blast', 'party_damage', 6.3, 16, 'The furnace vents superheated air across the party.'), + (21, 22, 'Melting Touch', 'dispellable_dot', 9.1, 11, 'A target begins to melt from intense heat.'), + (100, 102, 'Caldera Shockwave', 'party_damage', 5.6, 14, 'A shockwave rolls across all ten raiders.'), + (101, 102, 'Gatebrand', 'dispellable_dot', 8.2, 9, 'Brands one raider with a seal of living fire.'), + (102, 105, 'Pillar of Judgment', 'party_damage', 5.2, 16, 'Columns of flame erupt beneath the raid.'), + (103, 105, 'Inquisitor''s Brand', 'dispellable_dot', 7.8, 10, 'A burning brand persists until cleansed.'), + (104, 108, 'Crownflare', 'party_damage', 4.9, 18, 'The Ember Crown releases a raid-wide flare.'), + (105, 108, 'Royal Decree', 'dispellable_dot', 7.1, 12, 'A lethal decree marks one raider for cleansing.'); + +INSERT OR IGNORE INTO classes + (id, slug, name, resource_name, max_resource, theme_color, description) +VALUES + (1, 'dawnweaver', 'Dawnweaver', 'Mana', 100, '#e5b95f', 'A reactive healer using radiant restoration and protective wards.'), + (2, 'lifebinder', 'Lifebinder', 'Bloom', 100, '#4fb978', 'A patient healer who grows powerful restorative effects over time.'), + (3, 'runesage', 'Runesage', 'Focus', 100, '#708bd6', 'A tactical healer who prepares runes, barriers, and delayed recovery.'); + +INSERT OR IGNORE INTO spells + (id, class_id, slug, name, spell_type, resource_cost, cooldown_seconds, power, unlock_level, glyph, description) +VALUES + (1, 1, 'mend', 'Mend', 'direct_heal', 5, 0.5, 30, 1, '+', 'A fast, efficient single-target heal.'), + (2, 1, 'renew', 'Renew', 'heal_over_time', 7, 0.5, 12, 1, '~', 'Heals now and continues healing over time.'), + (3, 1, 'radiance', 'Radiance', 'party_heal', 12, 8, 18, 1, '*', 'Restores health to every living party member.'), + (4, 1, 'sun-ward', 'Sun Ward', 'absorb', 8, 7, 36, 1, 'O', 'Places a damage-absorbing shield on your target.'), + (5, 1, 'purify', 'Purify', 'cleanse', 5, 5, 10, 1, 'x', 'Removes a harmful effect and restores health.'), + (6, 1, 'dawn-burst', 'Dawn Burst', 'party_heal', 16, 12, 28, 5, 'D', 'A brilliant wave of healing for the entire party.'), + (7, 1, 'guardian-light', 'Guardian Light', 'absorb', 13, 14, 55, 10, 'G', 'A powerful ward reserved for moments of extreme danger.'), + (8, 1, 'second-sun', 'Second Sun', 'direct_heal', 20, 20, 85, 15, 'S', 'Calls down a delayed surge of restorative light.'), + (9, 1, 'daybreak', 'Daybreak', 'party_heal', 23, 30, 48, 20, 'A', 'Floods the party with the full strength of dawn.'), + (20, 2, 'verdant-touch', 'Verdant Touch', 'direct_heal', 5, 0.5, 28, 1, '+', 'A quick pulse of living energy.'), + (21, 2, 'seed-of-life', 'Seed of Life', 'heal_over_time', 7, 0.5, 11, 1, 's', 'Plants a restorative seed that blooms over time.'), + (22, 2, 'wild-bloom', 'Wild Bloom', 'party_heal', 12, 8, 17, 1, '*', 'Restorative growth spreads through the party.'), + (23, 2, 'barkskin', 'Barkskin', 'absorb', 8, 7, 34, 1, 'B', 'Wraps an ally in protective living bark.'), + (24, 2, 'purging-sap', 'Purging Sap', 'cleanse', 5, 5, 10, 1, 'p', 'Draws a harmful effect out through enchanted sap.'), + (25, 2, 'ancient-grove', 'Ancient Grove', 'party_heal', 17, 12, 31, 5, 'T', 'Briefly summons the shelter of an ancient grove.'), + (30, 3, 'etched-mend', 'Etched Mend', 'direct_heal', 5, 0.5, 29, 1, '+', 'Completes a simple rune of restoration.'), + (31, 3, 'echo-rune', 'Echo Rune', 'heal_over_time', 7, 0.5, 12, 1, 'e', 'Repeats a restorative rune over several moments.'), + (32, 3, 'concordance', 'Concordance', 'party_heal', 12, 8, 18, 1, '*', 'Links the party through a shared healing pattern.'), + (33, 3, 'aegis-script', 'Aegis Script', 'absorb', 8, 7, 35, 1, 'O', 'Writes a temporary barrier around an ally.'), + (34, 3, 'unravel', 'Unravel', 'cleanse', 5, 5, 10, 1, 'u', 'Unravels a hostile magical pattern.'), + (35, 3, 'grand-design', 'Grand Design', 'party_heal', 16, 12, 30, 5, 'R', 'Activates a prepared network of restorative runes.'); + +UPDATE spells SET resource_cost = CASE slug + WHEN 'mend' THEN 5 + WHEN 'renew' THEN 7 + WHEN 'radiance' THEN 12 + WHEN 'sun-ward' THEN 8 + WHEN 'purify' THEN 5 + WHEN 'dawn-burst' THEN 16 + WHEN 'guardian-light' THEN 13 + WHEN 'second-sun' THEN 20 + WHEN 'daybreak' THEN 23 + WHEN 'verdant-touch' THEN 5 + WHEN 'seed-of-life' THEN 7 + WHEN 'wild-bloom' THEN 12 + WHEN 'barkskin' THEN 8 + WHEN 'purging-sap' THEN 5 + WHEN 'ancient-grove' THEN 17 + WHEN 'etched-mend' THEN 5 + WHEN 'echo-rune' THEN 7 + WHEN 'concordance' THEN 12 + WHEN 'aegis-script' THEN 8 + WHEN 'unravel' THEN 5 + WHEN 'grand-design' THEN 16 + ELSE resource_cost +END; + +UPDATE spells SET unlock_level = 1, glyph = '+' WHERE slug = 'mend'; +UPDATE spells SET unlock_level = 1, glyph = '~' WHERE slug = 'renew'; +UPDATE spells SET unlock_level = 1, glyph = '*' WHERE slug = 'radiance'; +UPDATE spells SET unlock_level = 1, glyph = 'O' WHERE slug = 'sun-ward'; +UPDATE spells SET unlock_level = 1, glyph = 'x' WHERE slug = 'purify'; +UPDATE spells SET cooldown_seconds = 0.5 +WHERE slug IN ('mend', 'renew', 'verdant-touch', 'seed-of-life', 'etched-mend', 'echo-rune'); +UPDATE spells SET unlock_level = 5 WHERE slug IN ('dawn-burst', 'ancient-grove', 'grand-design'); +UPDATE spells SET unlock_level = 10 WHERE slug = 'guardian-light'; +UPDATE spells SET unlock_level = 15 WHERE slug = 'second-sun'; +UPDATE spells SET unlock_level = 20 WHERE slug = 'daybreak'; + +INSERT OR IGNORE INTO items + (id, slug, name, slot, rarity, item_level, healing_power, max_resource_bonus, glyph, description) +VALUES + (1, 'emberglass-sigil', 'Emberglass Sigil', 'ring', 'rare', 5, 4, 5, 'o', 'Warm light moves inside the cracked glass.'), + (2, 'wardens-cinderwrap', 'Warden''s Cinderwrap', 'chest', 'uncommon', 5, 3, 0, 'C', 'Robes threaded with fire-resistant fibers.'), + (3, 'ashwood-crook', 'Ashwood Crook', 'weapon', 'uncommon', 5, 5, 0, '/', 'A healer''s staff carved from fire-hardened wood.'), + (4, 'cinderstep-boots', 'Cinderstep Boots', 'boots', 'uncommon', 5, 3, 0, 'b', 'Light boots made for crossing burning ground.'), + (5, 'adepts-hood', 'Adept''s Hood', 'helmet', 'uncommon', 5, 3, 4, '^', 'The inner cloth is covered in careful annotations.'), + (6, 'furnace-tenders-wraps', 'Furnace Tender''s Wraps', 'gloves', 'uncommon', 5, 3, 2, 'g', 'Heat-scored gloves that still hum with careful magic.'), + (7, 'warden-ember', 'Warden''s Ember', 'trinket', 'rare', 5, 4, 4, '*', 'A coal from the old furnace that refuses to cool.'), + (201, 'tempered-emberglass-sigil', 'Tempered Emberglass Sigil', 'ring', 'rare', 10, 7, 9, 'o', 'The glass has been tempered by Veteran flame.'), + (202, 'tempered-cinderwrap', 'Tempered Cinderwrap', 'chest', 'uncommon', 10, 7, 2, 'C', 'Reinforced robes woven for Veteran expeditions.'), + (203, 'tempered-ashwood-crook', 'Tempered Ashwood Crook', 'weapon', 'uncommon', 10, 10, 2, '/', 'A fire-hardened staff balanced for Veteran healers.'), + (204, 'tempered-cinderstep-boots', 'Tempered Cinderstep Boots', 'boots', 'uncommon', 10, 6, 5, 'b', 'Sturdy boots for crossing hotter ground.'), + (205, 'tempered-adepts-hood', 'Tempered Adept''s Hood', 'helmet', 'uncommon', 10, 6, 6, '^', 'Its annotations glow when danger approaches.'), + (206, 'tempered-furnace-wraps', 'Tempered Furnace Wraps', 'gloves', 'uncommon', 10, 7, 4, 'g', 'Veteran wraps stitched with ember-resistant thread.'), + (207, 'tempered-warden-ember', 'Tempered Warden''s Ember', 'trinket', 'rare', 10, 8, 7, '*', 'A stable ember pulsing with restorative heat.'), + (301, 'runed-emberglass-sigil', 'Runed Emberglass Sigil', 'ring', 'rare', 15, 10, 13, 'o', 'Champion runes swim beneath the emberglass.'), + (302, 'runed-cinderwrap', 'Runed Cinderwrap', 'chest', 'rare', 15, 11, 3, 'C', 'Champion robes covered in protective script.'), + (303, 'runed-ashwood-crook', 'Runed Ashwood Crook', 'weapon', 'rare', 15, 15, 3, '/', 'A Champion focus carved with radiant channels.'), + (304, 'runed-cinderstep-boots', 'Runed Cinderstep Boots', 'boots', 'rare', 15, 9, 8, 'b', 'Each step leaves a fading restorative rune.'), + (305, 'runed-adepts-hood', 'Runed Adept''s Hood', 'helmet', 'rare', 15, 9, 9, '^', 'A Champion hood that sharpens restorative focus.'), + (306, 'runed-furnace-wraps', 'Runed Furnace Wraps', 'gloves', 'rare', 15, 11, 6, 'g', 'Runes brighten as healing magic passes through them.'), + (307, 'runed-warden-ember', 'Runed Warden''s Ember', 'trinket', 'rare', 15, 12, 10, '*', 'A Champion ember bound by concentric runes.'), + (401, 'mythic-emberglass-sigil', 'Mythic Emberglass Sigil', 'ring', 'epic', 20, 14, 17, 'o', 'A flawless sigil carrying the furnace''s true name.'), + (402, 'mythic-cinderwrap', 'Mythic Cinderwrap', 'chest', 'epic', 20, 15, 4, 'C', 'Mythic vestments untouched by ordinary flame.'), + (403, 'mythic-ashwood-crook', 'Mythic Ashwood Crook', 'weapon', 'epic', 20, 20, 4, '/', 'A Mythic focus cut from the oldest ashwood.'), + (404, 'mythic-cinderstep-boots', 'Mythic Cinderstep Boots', 'boots', 'epic', 20, 12, 11, 'b', 'The wearer walks safely across living fire.'), + (405, 'mythic-adepts-hood', 'Mythic Adept''s Hood', 'helmet', 'epic', 20, 12, 12, '^', 'A Mythic hood filled with whispered formulae.'), + (406, 'mythic-furnace-wraps', 'Mythic Furnace Wraps', 'gloves', 'epic', 20, 15, 8, 'g', 'Mythic gloves that shape healing into precise patterns.'), + (407, 'mythic-warden-ember', 'Mythic Warden''s Ember', 'trinket', 'epic', 20, 16, 13, '*', 'The furnace heart answers this ember.'), + (501, 'ascendant-emberglass-sigil', 'Ascendant Emberglass Sigil', 'ring', 'epic', 25, 18, 21, 'o', 'An Ascendant sigil radiant with contained dawn.'), + (502, 'ascendant-cinderwrap', 'Ascendant Cinderwrap', 'chest', 'epic', 25, 19, 5, 'C', 'Ascendant robes woven from fire and light.'), + (503, 'ascendant-ashwood-crook', 'Ascendant Ashwood Crook', 'weapon', 'epic', 25, 25, 5, '/', 'An Ascendant staff bearing an unending ember.'), + (504, 'ascendant-cinderstep-boots', 'Ascendant Cinderstep Boots', 'boots', 'epic', 25, 15, 14, 'b', 'The ground cools before each Ascendant step.'), + (505, 'ascendant-adepts-hood', 'Ascendant Adept''s Hood', 'helmet', 'epic', 25, 15, 15, '^', 'Every healing pattern appears obvious beneath this hood.'), + (506, 'ascendant-furnace-wraps', 'Ascendant Furnace Wraps', 'gloves', 'epic', 25, 19, 10, 'g', 'Ascendant gloves threaded with concentrated daylight.'), + (507, 'ascendant-warden-ember', 'Ascendant Warden''s Ember', 'trinket', 'epic', 25, 20, 16, '*', 'A perfect ember from the heart of the Ashen Halls.'), + (701, 'caldera-signet', 'Caldera Signet', 'ring', 'rare', 7, 5, 6, 'o', 'A raid-forged signet warm with caldera light.'), + (702, 'vanguard-mantle', 'Vanguard Mantle', 'chest', 'rare', 7, 5, 1, 'C', 'A reinforced mantle taken from the citadel vanguard.'), + (703, 'pyrebinder-crook', 'Pyrebinder Crook', 'weapon', 'rare', 7, 7, 1, '/', 'A focus used to bend living flame toward restoration.'), + (704, 'emberstep-treads', 'Emberstep Treads', 'boots', 'rare', 7, 4, 5, 'b', 'Raid treads made for crossing unstable volcanic stone.'), + (705, 'gatekeeper-cowl', 'Gatekeeper Cowl', 'helmet', 'rare', 7, 4, 6, '^', 'A cowl inscribed with the wards of the outer gate.'), + (706, 'crownward-wraps', 'Crownward Wraps', 'gloves', 'rare', 7, 5, 3, 'g', 'Precise wraps worn by the healers of the Ember Crown.'), + (707, 'living-coal-charm', 'Living Coal Charm', 'trinket', 'rare', 7, 6, 5, '*', 'A coal that pulses in time with nearby heartbeats.'), + (710, 'royal-caldera-signet', 'Royal Caldera Signet', 'ring', 'epic', 10, 8, 9, 'o', 'A royal signet claimed after breaking the Ember Crown.'), + (711, 'ember-crown-vestment', 'Ember Crown Vestment', 'chest', 'epic', 10, 8, 2, 'C', 'The ceremonial vestment of the fallen crown court.'), + (712, 'crownshard-crook', 'Crownshard Crook', 'weapon', 'epic', 10, 11, 2, '/', 'A healing focus set with a cooled shard of the crown.'), + (713, 'caldera-walkers', 'Caldera Walkers', 'boots', 'epic', 10, 7, 8, 'b', 'Boots that remain cool even at the caldera''s heart.'), + (714, 'inquisitors-cowl', 'Inquisitor''s Cowl', 'helmet', 'epic', 10, 7, 9, '^', 'The cowl of an inquisitor, stripped of its cruel sigils.'), + (715, 'royal-flame-wraps', 'Royal Flame Wraps', 'gloves', 'epic', 10, 8, 6, 'g', 'Royal wraps that shape flame into restorative patterns.'), + (716, 'extinguished-crown', 'Extinguished Crown', 'trinket', 'epic', 10, 9, 8, '*', 'A harmless fragment of the once-living Ember Crown.'), + (100, 'novice-crook', 'Novice Crook', 'weapon', 'common', 1, 1, 0, '/', 'A simple focus carried by every new healer.'), + (101, 'novice-cowl', 'Novice Cowl', 'helmet', 'common', 1, 0, 1, '^', 'Plain cloth that helps steady a beginner''s thoughts.'), + (102, 'novice-vestment', 'Novice Vestment', 'chest', 'common', 1, 1, 0, 'C', 'Unadorned robes issued to apprentice healers.'), + (103, 'novice-wraps', 'Novice Wraps', 'gloves', 'common', 1, 1, 0, 'g', 'Soft wraps that leave the fingers free for spellwork.'), + (104, 'novice-slippers', 'Novice Slippers', 'boots', 'common', 1, 0, 1, 'b', 'Comfortable enough for a first expedition.'), + (105, 'novice-band', 'Novice Band', 'ring', 'common', 1, 0, 1, 'o', 'A copper band etched with a minor restorative rune.'), + (106, 'novice-token', 'Novice Token', 'trinket', 'common', 1, 1, 0, '*', 'A small charm commemorating the healer''s oath.'), + (107, 'novice-wand', 'Novice Wand', 'weapon', 'common', 1, 0, 2, '!', 'A spare focus that favors a deeper resource pool.'), + (600, 'minor-component', 'Minor Component', 'component', 'common', 1, 0, 0, '◆', 'A basic crafting component.'), + (601, 'basic-component', 'Basic Component', 'component', 'common', 5, 0, 0, '◇', 'A standard crafting component.'), + (602, 'refined-component', 'Refined Component', 'component', 'common', 10, 0, 0, '◈', 'A refined crafting component.'), + (603, 'advanced-component', 'Advanced Component', 'component', 'common', 15, 0, 0, '◉', 'An advanced crafting component.'), + (604, 'superior-component', 'Superior Component', 'component', 'common', 20, 0, 0, '◎', 'A superior crafting component.'), + (605, 'primal-component', 'Primal Component', 'component', 'common', 25, 0, 0, '✦', 'A primal crafting component.'); + +INSERT OR IGNORE INTO items + (id, slug, name, slot, rarity, item_level, healing_power, max_resource_bonus, glyph, description) +VALUES + (8, 'ashwalker-legwraps', 'Ashwalker Legwraps', 'pants', 'uncommon', 5, 3, 3, 'P', 'Layered legwraps brushed clean of furnace ash.'), + (9, 'sootglass-pendant', 'Sootglass Pendant', 'necklace', 'rare', 5, 4, 4, 'n', 'A pendant made from glass cooled in living soot.'), + (208, 'tempered-ashwalker-legwraps', 'Tempered Ashwalker Legwraps', 'pants', 'uncommon', 10, 6, 6, 'P', 'Veteran legwraps stitched with tempered ember thread.'), + (209, 'tempered-sootglass-pendant', 'Tempered Sootglass Pendant', 'necklace', 'rare', 10, 8, 7, 'n', 'The sootglass holds a steadier restorative glow.'), + (308, 'runed-ashwalker-legwraps', 'Runed Ashwalker Legwraps', 'pants', 'rare', 15, 9, 9, 'P', 'Champion runes keep each step measured and sure.'), + (309, 'runed-sootglass-pendant', 'Runed Sootglass Pendant', 'necklace', 'rare', 15, 12, 10, 'n', 'Fine runes circle the sootglass pendant.'), + (408, 'mythic-ashwalker-legwraps', 'Mythic Ashwalker Legwraps', 'pants', 'epic', 20, 12, 12, 'P', 'Mythic legwraps that shed molten grit.'), + (409, 'mythic-sootglass-pendant', 'Mythic Sootglass Pendant', 'necklace', 'epic', 20, 16, 13, 'n', 'A flawless pendant carrying furnace-born warmth.'), + (508, 'ascendant-ashwalker-legwraps', 'Ascendant Ashwalker Legwraps', 'pants', 'epic', 25, 15, 15, 'P', 'Ascendant legwraps woven from fireproof daylight.'), + (509, 'ascendant-sootglass-pendant', 'Ascendant Sootglass Pendant', 'necklace', 'epic', 25, 20, 16, 'n', 'The sootglass burns with a contained dawn.'), + (708, 'caldera-legwraps', 'Caldera Legwraps', 'pants', 'rare', 7, 4, 6, 'P', 'Raid legwraps made for unstable volcanic stone.'), + (709, 'gateflame-pendant', 'Gateflame Pendant', 'necklace', 'rare', 7, 6, 5, 'n', 'A pendant bearing a cooled mote of gateflame.'), + (717, 'royal-caldera-legwraps', 'Royal Caldera Legwraps', 'pants', 'epic', 10, 7, 9, 'P', 'Royal legwraps recovered from the Ember Crown court.'), + (718, 'ember-crown-pendant', 'Ember Crown Pendant', 'necklace', 'epic', 10, 9, 8, 'n', 'A pendant set with a harmless shard of the Ember Crown.'), + (108, 'novice-trousers', 'Novice Trousers', 'pants', 'common', 1, 0, 1, 'P', 'Plain trousers for a first expedition.'), + (109, 'novice-pendant', 'Novice Pendant', 'necklace', 'common', 1, 1, 0, 'n', 'A simple charm worn by apprentice healers.'), + (800, 'ashen-cowl-pattern', 'Ashen Cowl Pattern', 'component', 'common', 5, 0, 0, 'H', 'A Warden Vhal pattern used to craft helmets.'), + (801, 'ashen-vestment-pattern', 'Ashen Vestment Pattern', 'component', 'common', 5, 0, 0, 'C', 'A Warden Vhal pattern used to craft chest pieces.'), + (802, 'ashen-wrap-pattern', 'Ashen Wrap Pattern', 'component', 'common', 5, 0, 0, 'G', 'A Warden Vhal pattern used to craft gloves.'), + (803, 'cinderstep-pattern', 'Cinderstep Pattern', 'component', 'common', 5, 0, 0, 'B', 'A Forge-Priestess Haela pattern used to craft boots.'), + (804, 'emberglass-setting', 'Emberglass Setting', 'component', 'common', 5, 0, 0, 'R', 'A Forge-Priestess Haela setting used to craft rings.'), + (805, 'warden-ember-core', 'Warden Ember Core', 'component', 'common', 5, 0, 0, 'T', 'A Forge-Priestess Haela core used to craft trinkets.'), + (806, 'ashwood-focus-pattern', 'Ashwood Focus Pattern', 'component', 'common', 5, 0, 0, 'W', 'An Old Furnace pattern used to craft weapons.'), + (807, 'furnace-legwrap-pattern', 'Furnace Legwrap Pattern', 'component', 'common', 5, 0, 0, 'L', 'An Old Furnace pattern used to craft pants.'), + (808, 'sootglass-pendant-pattern', 'Sootglass Pendant Pattern', 'component', 'common', 5, 0, 0, 'N', 'An Old Furnace pattern used to craft necklaces.'), + (820, 'vhal-emberplate', 'Vhal Emberplate', 'component', 'uncommon', 5, 0, 0, 'V', 'A boss material from Warden Vhal.'), + (821, 'haela-forgebrand', 'Haela Forgebrand', 'component', 'uncommon', 5, 0, 0, 'F', 'A boss material from Forge-Priestess Haela.'), + (822, 'old-furnace-heartshard', 'Old Furnace Heartshard', 'component', 'uncommon', 5, 0, 0, 'O', 'A boss material from the Old Furnace.'), + (830, 'gatekeeper-cowl-pattern', 'Gatekeeper Cowl Pattern', 'component', 'rare', 10, 0, 0, 'H', 'A Gatekeeper Arkon pattern used to craft raid helmets.'), + (831, 'crownward-vestment-pattern', 'Crownward Vestment Pattern', 'component', 'rare', 10, 0, 0, 'C', 'A Gatekeeper Arkon pattern used to craft raid chest pieces.'), + (832, 'crownward-wrap-pattern', 'Crownward Wrap Pattern', 'component', 'rare', 10, 0, 0, 'G', 'A Gatekeeper Arkon pattern used to craft raid gloves.'), + (833, 'caldera-tread-pattern', 'Caldera Tread Pattern', 'component', 'rare', 10, 0, 0, 'B', 'A High Inquisitor Vael pattern used to craft raid boots.'), + (834, 'royal-signet-setting', 'Royal Signet Setting', 'component', 'rare', 10, 0, 0, 'R', 'A High Inquisitor Vael setting used to craft raid rings.'), + (835, 'living-coal-vessel', 'Living Coal Vessel', 'component', 'rare', 10, 0, 0, 'T', 'A High Inquisitor Vael vessel used to craft raid trinkets.'), + (836, 'crownshard-focus-pattern', 'Crownshard Focus Pattern', 'component', 'rare', 10, 0, 0, 'W', 'An Ember Crown pattern used to craft raid weapons.'), + (837, 'inquisitor-legwrap-pattern', 'Inquisitor Legwrap Pattern', 'component', 'rare', 10, 0, 0, 'L', 'An Ember Crown pattern used to craft raid pants.'), + (838, 'crown-pendant-pattern', 'Crown Pendant Pattern', 'component', 'rare', 10, 0, 0, 'N', 'An Ember Crown pattern used to craft raid necklaces.'), + (850, 'arkon-gate-sigil', 'Arkon Gate Sigil', 'component', 'rare', 10, 0, 0, 'A', 'A raid boss material from Gatekeeper Arkon.'), + (851, 'vael-brandseal', 'Vael Brandseal', 'component', 'rare', 10, 0, 0, 'I', 'A raid boss material from High Inquisitor Vael.'), + (852, 'ember-crown-shard', 'Ember Crown Shard', 'component', 'rare', 10, 0, 0, 'E', 'A raid boss material from the Ember Crown.'), + (860, 'bulldrome-hide', 'Bulldrome Hide', 'component', 'uncommon', 5, 0, 0, 'H', 'A rugged hide from Bulldrome.'), + (861, 'bullfango-head', 'Bullfango Head', 'component', 'rare', 5, 0, 0, 'F', 'A rare head taken from a Bullfango.'), + (862, 'bulldrome-tusk', 'Bulldrome Tusk', 'component', 'uncommon', 5, 0, 0, 'T', 'A heavy tusk from Bulldrome.'), + (863, 'giant-bone', 'Giant Bone', 'component', 'uncommon', 5, 0, 0, 'B', 'A giant bone used in sturdy crafting.'), + (864, 'kut-ku-webbing', 'Kut-Ku Webbing', 'component', 'uncommon', 5, 0, 0, 'W', 'Flexible webbing from Yian Kut-Ku.'), + (865, 'kut-ku-shell', 'Kut-Ku Shell', 'component', 'uncommon', 5, 0, 0, 'S', 'A bright shell from Yian Kut-Ku.'), + (866, 'kut-ku-scale', 'Kut-Ku Scale', 'component', 'uncommon', 5, 0, 0, 'K', 'A scale from Yian Kut-Ku.'), + (867, 'giant-beak', 'Giant Beak', 'component', 'rare', 5, 0, 0, 'G', 'A rare giant beak from Yian Kut-Ku.'), + (868, 'rathian-shell', 'Rathian Shell', 'component', 'uncommon', 5, 0, 0, 'R', 'A durable shell from Rathian.'), + (869, 'rathian-scale-x2', 'Rathian Scale x2', 'component', 'uncommon', 5, 0, 0, '2', 'A paired Rathian scale reward.'), + (870, 'rathian-spike', 'Rathian Spike', 'component', 'rare', 5, 0, 0, 'P', 'A sharp spike from Rathian.'), + (871, 'rathian-plate', 'Rathian Plate', 'component', 'epic', 5, 0, 0, '*', 'A prized Rathian plate.'); + +UPDATE items SET slot = 'ring', item_level = 5 WHERE slug = 'emberglass-sigil'; +UPDATE items SET item_level = 5 WHERE slug = 'wardens-cinderwrap'; +UPDATE items SET glyph = 'o' WHERE slug = 'emberglass-sigil'; +UPDATE items SET glyph = 'C' WHERE slug = 'wardens-cinderwrap'; +UPDATE items SET glyph = '/' WHERE slug = 'ashwood-crook'; +UPDATE items SET glyph = 'b' WHERE slug = 'cinderstep-boots'; +UPDATE items SET glyph = '^' WHERE slug = 'adepts-hood'; + +DELETE FROM encounter_loot; + +INSERT INTO encounter_loot + (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) +VALUES + (3, 860, 1, 50, 1.0), (3, 861, 1, 5, 1.0), (3, 862, 1, 15, 1.0), (3, 863, 1, 30, 1.0), + (3, 860, 2, 50, 1.0), (3, 861, 2, 5, 1.0), (3, 862, 2, 15, 1.0), (3, 863, 2, 30, 1.0), + (3, 860, 3, 50, 1.0), (3, 861, 3, 5, 1.0), (3, 862, 3, 15, 1.0), (3, 863, 3, 30, 1.0), + (3, 860, 4, 50, 1.0), (3, 861, 4, 5, 1.0), (3, 862, 4, 15, 1.0), (3, 863, 4, 30, 1.0), + (3, 860, 5, 50, 1.0), (3, 861, 5, 5, 1.0), (3, 862, 5, 15, 1.0), (3, 863, 5, 30, 1.0), + (12, 864, 1, 32, 1.0), (12, 865, 1, 50, 1.0), (12, 866, 1, 13, 1.0), (12, 867, 1, 5, 1.0), + (12, 864, 2, 32, 1.0), (12, 865, 2, 50, 1.0), (12, 866, 2, 13, 1.0), (12, 867, 2, 5, 1.0), + (12, 864, 3, 32, 1.0), (12, 865, 3, 50, 1.0), (12, 866, 3, 13, 1.0), (12, 867, 3, 5, 1.0), + (12, 864, 4, 32, 1.0), (12, 865, 4, 50, 1.0), (12, 866, 4, 13, 1.0), (12, 867, 4, 5, 1.0), + (12, 864, 5, 32, 1.0), (12, 865, 5, 50, 1.0), (12, 866, 5, 13, 1.0), (12, 867, 5, 5, 1.0), + (22, 868, 1, 36, 1.0), (22, 869, 1, 52, 1.0), (22, 870, 1, 10, 1.0), (22, 871, 1, 2, 1.0), + (22, 868, 2, 36, 1.0), (22, 869, 2, 52, 1.0), (22, 870, 2, 10, 1.0), (22, 871, 2, 2, 1.0), + (22, 868, 3, 36, 1.0), (22, 869, 3, 52, 1.0), (22, 870, 3, 10, 1.0), (22, 871, 3, 2, 1.0), + (22, 868, 4, 36, 1.0), (22, 869, 4, 52, 1.0), (22, 870, 4, 10, 1.0), (22, 871, 4, 2, 1.0), + (22, 868, 5, 36, 1.0), (22, 869, 5, 52, 1.0), (22, 870, 5, 10, 1.0), (22, 871, 5, 2, 1.0), + (102, 602, 101, 24, 1.0), (102, 850, 101, 22, 1.0), (102, 830, 101, 18, 1.0), (102, 831, 101, 18, 1.0), (102, 832, 101, 18, 1.0), + (105, 602, 101, 24, 1.0), (105, 851, 101, 22, 1.0), (105, 833, 101, 18, 1.0), (105, 834, 101, 18, 1.0), (105, 835, 101, 18, 1.0), + (108, 602, 101, 24, 1.0), (108, 852, 101, 22, 1.0), (108, 836, 101, 18, 1.0), (108, 837, 101, 18, 1.0), (108, 838, 101, 18, 1.0); + +DELETE FROM crafting_recipe_components; +DELETE FROM crafting_recipes; + +INSERT INTO crafting_recipes + (id, item_id, difficulty_id, source_dungeon_id, source_encounter_id) +VALUES + (1001, 5, 1, 1, 3), (1002, 2, 1, 1, 3), (1003, 6, 1, 1, 3), + (1004, 4, 1, 1, 12), (1005, 1, 1, 1, 12), (1006, 7, 1, 1, 12), + (1007, 3, 1, 1, 22), (1008, 8, 1, 1, 22), (1009, 9, 1, 1, 22), + (1101, 205, 2, 1, 3), (1102, 202, 2, 1, 3), (1103, 206, 2, 1, 3), + (1104, 204, 2, 1, 12), (1105, 201, 2, 1, 12), (1106, 207, 2, 1, 12), + (1107, 203, 2, 1, 22), (1108, 208, 2, 1, 22), (1109, 209, 2, 1, 22), + (1201, 305, 3, 1, 3), (1202, 302, 3, 1, 3), (1203, 306, 3, 1, 3), + (1204, 304, 3, 1, 12), (1205, 301, 3, 1, 12), (1206, 307, 3, 1, 12), + (1207, 303, 3, 1, 22), (1208, 308, 3, 1, 22), (1209, 309, 3, 1, 22), + (1301, 405, 4, 1, 3), (1302, 402, 4, 1, 3), (1303, 406, 4, 1, 3), + (1304, 404, 4, 1, 12), (1305, 401, 4, 1, 12), (1306, 407, 4, 1, 12), + (1307, 403, 4, 1, 22), (1308, 408, 4, 1, 22), (1309, 409, 4, 1, 22), + (1401, 505, 5, 1, 3), (1402, 502, 5, 1, 3), (1403, 506, 5, 1, 3), + (1404, 504, 5, 1, 12), (1405, 501, 5, 1, 12), (1406, 507, 5, 1, 12), + (1407, 503, 5, 1, 22), (1408, 508, 5, 1, 22), (1409, 509, 5, 1, 22), + (2001, 714, 101, 2, 102), (2002, 711, 101, 2, 102), (2003, 715, 101, 2, 102), + (2004, 713, 101, 2, 105), (2005, 710, 101, 2, 105), (2006, 716, 101, 2, 105), + (2007, 712, 101, 2, 108), (2008, 717, 101, 2, 108), (2009, 718, 101, 2, 108); + +WITH recipe_requirements(recipe_id, tier_component_id, tier_qty, boss_component_id, pattern_component_id) AS ( + VALUES + (1001, 601, 1, 820, 800), (1002, 601, 1, 820, 801), (1003, 601, 1, 820, 802), + (1004, 601, 1, 821, 803), (1005, 601, 1, 821, 804), (1006, 601, 1, 821, 805), + (1007, 601, 1, 822, 806), (1008, 601, 1, 822, 807), (1009, 601, 1, 822, 808), + (1101, 602, 2, 820, 800), (1102, 602, 2, 820, 801), (1103, 602, 2, 820, 802), + (1104, 602, 2, 821, 803), (1105, 602, 2, 821, 804), (1106, 602, 2, 821, 805), + (1107, 602, 2, 822, 806), (1108, 602, 2, 822, 807), (1109, 602, 2, 822, 808), + (1201, 603, 3, 820, 800), (1202, 603, 3, 820, 801), (1203, 603, 3, 820, 802), + (1204, 603, 3, 821, 803), (1205, 603, 3, 821, 804), (1206, 603, 3, 821, 805), + (1207, 603, 3, 822, 806), (1208, 603, 3, 822, 807), (1209, 603, 3, 822, 808), + (1301, 604, 4, 820, 800), (1302, 604, 4, 820, 801), (1303, 604, 4, 820, 802), + (1304, 604, 4, 821, 803), (1305, 604, 4, 821, 804), (1306, 604, 4, 821, 805), + (1307, 604, 4, 822, 806), (1308, 604, 4, 822, 807), (1309, 604, 4, 822, 808), + (1401, 605, 5, 820, 800), (1402, 605, 5, 820, 801), (1403, 605, 5, 820, 802), + (1404, 605, 5, 821, 803), (1405, 605, 5, 821, 804), (1406, 605, 5, 821, 805), + (1407, 605, 5, 822, 806), (1408, 605, 5, 822, 807), (1409, 605, 5, 822, 808), + (2001, 602, 3, 850, 830), (2002, 602, 3, 850, 831), (2003, 602, 3, 850, 832), + (2004, 602, 3, 851, 833), (2005, 602, 3, 851, 834), (2006, 602, 3, 851, 835), + (2007, 602, 3, 852, 836), (2008, 602, 3, 852, 837), (2009, 602, 3, 852, 838) +) +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) +SELECT recipe_id, tier_component_id, tier_qty FROM recipe_requirements +UNION ALL +SELECT recipe_id, boss_component_id, 1 FROM recipe_requirements +UNION ALL +SELECT recipe_id, pattern_component_id, 1 FROM recipe_requirements; + +DELETE FROM item_set_bonuses; +DELETE FROM item_set_items; +DELETE FROM item_sets; + +INSERT INTO item_sets (id, slug, name, description) VALUES + (1, 'ember-crown-regalia', 'Ember Crown Regalia', 'A raid set crafted from Citadel of the Ember Crown materials.'); + +INSERT INTO item_set_items (set_id, item_id) VALUES + (1, 710), (1, 711), (1, 712), (1, 713), (1, 714), (1, 715), (1, 716), (1, 717), (1, 718); + +INSERT INTO item_set_bonuses + (id, set_id, required_pieces, effect_type, description) +VALUES + (1, 1, 2, 'mend_extra_target', 'Mend heals 1 additional target.'), + (2, 1, 4, 'renew_extra_target', 'Renew is applied to 1 additional target.'), + (3, 1, 7, 'mend_applies_renew', 'Casting Mend on a target applies Renew.'); + +DELETE FROM dungeon_completion_loot; + +INSERT OR IGNORE INTO characters (id, class_id, name, level, experience) VALUES + (1, 1, 'Mira', 1, 0), + (2, 2, 'Mira', 1, 0), + (3, 3, 'Mira', 1, 0); + +INSERT OR IGNORE INTO character_ability_slots (character_id, slot_number, spell_id) VALUES + (1, 1, 1), (1, 2, 2), (1, 3, 3), (1, 4, 4), (1, 5, 5), (1, 6, NULL), + (2, 1, 20), (2, 2, 21), (2, 3, 22), (2, 4, 23), (2, 5, 24), (2, 6, NULL), + (3, 1, 30), (3, 2, 31), (3, 3, 32), (3, 4, 33), (3, 5, 34), (3, 6, NULL); + +INSERT OR IGNORE INTO character_inventory (character_id, item_id, quantity, equipped) VALUES + (1, 100, 1, 1), (1, 101, 1, 1), (1, 102, 1, 1), (1, 103, 1, 1), + (1, 104, 1, 1), (1, 108, 1, 1), (1, 105, 1, 1), (1, 109, 1, 1), (1, 106, 1, 1), (1, 107, 1, 0), + (2, 100, 1, 1), (2, 101, 1, 1), (2, 102, 1, 1), (2, 103, 1, 1), + (2, 104, 1, 1), (2, 108, 1, 1), (2, 105, 1, 1), (2, 109, 1, 1), (2, 106, 1, 1), (2, 107, 1, 0), + (3, 100, 1, 1), (3, 101, 1, 1), (3, 102, 1, 1), (3, 103, 1, 1), + (3, 104, 1, 1), (3, 108, 1, 1), (3, 105, 1, 1), (3, 109, 1, 1), (3, 106, 1, 1), (3, 107, 1, 0); + +INSERT OR IGNORE INTO talents + (id, class_id, slug, name, max_rank, tier, branch, prerequisite_talent_id, prerequisite_rank, effect_type, effect_value_per_rank, glyph, description) +VALUES + (1, 1, 'bright-reserves', 'Bright Reserves', 5, 1, 1, NULL, 0, 'max_resource', 2, 'M', 'Increases maximum Mana by 2 per rank.'), + (2, 1, 'gentle-dawn', 'Gentle Dawn', 5, 1, 2, NULL, 0, 'hot_power_percent', 2, '~', 'Increases healing-over-time power by 2% per rank.'), + (10, 1, 'steady-hands', 'Steady Hands', 5, 1, 3, NULL, 0, 'direct_heal_percent', 2, '+', 'Increases direct healing by 2% per rank.'), + (11, 1, 'overflowing-light', 'Overflowing Light', 5, 2, 1, 1, 3, 'resource_regen_percent', 2, 'O', 'Improves Mana regeneration by 2% per rank. Requires Bright Reserves rank 3.'), + (12, 1, 'lingering-rays', 'Lingering Rays', 5, 2, 2, 2, 3, 'hot_duration_percent', 4, 'L', 'Extends healing-over-time duration by 4% per rank. Requires Gentle Dawn rank 3.'), + (13, 1, 'radiant-precision', 'Radiant Precision', 5, 2, 3, 10, 3, 'critical_heal_percent', 1, '!', 'Adds 1% healing critical chance per rank. Requires Steady Hands rank 3.'), + (14, 1, 'sunlit-aegis', 'Sunlit Aegis', 3, 3, 1, 11, 5, 'absorb_power_percent', 5, 'A', 'Strengthens absorb effects by 5% per rank. Requires Overflowing Light rank 5.'), + (15, 1, 'shared-dawn', 'Shared Dawn', 3, 3, 2, 12, 5, 'party_heal_percent', 5, '*', 'Increases party-wide healing by 5% per rank. Requires Lingering Rays rank 5.'), + (16, 1, 'miracle-worker', 'Miracle Worker', 1, 4, 2, 15, 3, 'cooldown_reduction_percent', 10, 'S', 'Reduces major healing cooldowns by 10%. Requires Shared Dawn rank 3.'), + + (3, 2, 'deep-roots', 'Deep Roots', 5, 1, 1, NULL, 0, 'max_resource', 2, 'R', 'Increases maximum Bloom by 2 per rank.'), + (20, 2, 'patient-growth', 'Patient Growth', 5, 1, 2, NULL, 0, 'hot_power_percent', 2, 's', 'Increases healing-over-time power by 2% per rank.'), + (21, 2, 'living-bark', 'Living Bark', 5, 1, 3, NULL, 0, 'absorb_power_percent', 2, 'B', 'Strengthens protective effects by 2% per rank.'), + (22, 2, 'spring-rain', 'Spring Rain', 5, 2, 1, 3, 3, 'resource_regen_percent', 2, 'r', 'Improves Bloom regeneration by 2% per rank. Requires Deep Roots rank 3.'), + (23, 2, 'perennial', 'Perennial', 5, 2, 2, 20, 3, 'hot_duration_percent', 4, 'P', 'Extends healing-over-time duration by 4% per rank. Requires Patient Growth rank 3.'), + (24, 2, 'sheltering-canopy', 'Sheltering Canopy', 5, 2, 3, 21, 3, 'party_heal_percent', 2, 'C', 'Improves healing on injured allies by 2% per rank. Requires Living Bark rank 3.'), + (25, 2, 'old-growth', 'Old Growth', 3, 3, 1, 22, 5, 'direct_heal_percent', 5, 'T', 'Strengthens direct healing by 5% per rank. Requires Spring Rain rank 5.'), + (26, 2, 'endless-season', 'Endless Season', 3, 3, 2, 23, 5, 'hot_power_percent', 5, 'E', 'Further improves periodic healing by 5% per rank. Requires Perennial rank 5.'), + (27, 2, 'heart-of-the-grove', 'Heart of the Grove', 1, 4, 2, 26, 3, 'cooldown_reduction_percent', 10, 'H', 'Reduces major healing cooldowns by 10%. Requires Endless Season rank 3.'), + + (4, 3, 'precise-script', 'Precise Script', 5, 1, 1, NULL, 0, 'direct_heal_percent', 2, '+', 'Increases direct healing by 2% per rank.'), + (40, 3, 'deep-focus', 'Deep Focus', 5, 1, 2, NULL, 0, 'max_resource', 2, 'F', 'Increases maximum Focus by 2 per rank.'), + (41, 3, 'reinforced-runes', 'Reinforced Runes', 5, 1, 3, NULL, 0, 'absorb_power_percent', 2, 'O', 'Strengthens barriers by 2% per rank.'), + (42, 3, 'efficient-lines', 'Efficient Lines', 5, 2, 1, 4, 3, 'resource_cost_percent', -2, 'E', 'Reduces ability costs by 2% per rank. Requires Precise Script rank 3.'), + (43, 3, 'resonant-pattern', 'Resonant Pattern', 5, 2, 2, 40, 3, 'party_heal_percent', 2, '*', 'Increases linked party healing by 2% per rank. Requires Deep Focus rank 3.'), + (44, 3, 'layered-aegis', 'Layered Aegis', 5, 2, 3, 41, 3, 'absorb_power_percent', 3, 'A', 'Further improves barriers by 3% per rank. Requires Reinforced Runes rank 3.'), + (45, 3, 'perfect-notation', 'Perfect Notation', 3, 3, 1, 42, 5, 'critical_heal_percent', 2, '!', 'Adds 2% healing critical chance per rank. Requires Efficient Lines rank 5.'), + (46, 3, 'grand-concordance', 'Grand Concordance', 3, 3, 2, 43, 5, 'party_heal_percent', 5, 'G', 'Improves party-wide healing by 5% per rank. Requires Resonant Pattern rank 5.'), + (47, 3, 'masterwork', 'Masterwork', 1, 4, 2, 46, 3, 'cooldown_reduction_percent', 10, 'M', 'Reduces major healing cooldowns by 10%. Requires Grand Concordance rank 3.'); + +UPDATE talents SET + branch = CASE slug + WHEN 'bright-reserves' THEN 1 + WHEN 'gentle-dawn' THEN 2 + WHEN 'deep-roots' THEN 1 + WHEN 'precise-script' THEN 1 + ELSE branch + END, + effect_type = CASE slug + WHEN 'bright-reserves' THEN 'max_resource' + WHEN 'gentle-dawn' THEN 'hot_power_percent' + WHEN 'deep-roots' THEN 'max_resource' + WHEN 'precise-script' THEN 'direct_heal_percent' + ELSE effect_type + END, + effect_value_per_rank = 2, + glyph = CASE slug + WHEN 'bright-reserves' THEN 'M' + WHEN 'gentle-dawn' THEN '~' + WHEN 'deep-roots' THEN 'R' + WHEN 'precise-script' THEN '+' + ELSE glyph + END +WHERE slug IN ('bright-reserves', 'gentle-dawn', 'deep-roots', 'precise-script'); + +UPDATE talents SET description = 'Increases maximum Mana by 2 per rank.' +WHERE slug = 'bright-reserves'; +UPDATE talents SET description = 'Increases healing-over-time power by 2% per rank.' +WHERE slug = 'gentle-dawn'; +UPDATE talents SET description = 'Increases maximum Bloom by 2 per rank.' +WHERE slug = 'deep-roots'; +UPDATE talents SET description = 'Increases direct healing by 2% per rank.' +WHERE slug = 'precise-script'; + +INSERT INTO game_settings (key, value) VALUES + ('max_level', '25'), + ('max_talent_points', '25'), + ('max_ability_slots', '6') +ON CONFLICT(key) DO UPDATE SET value = excluded.value; + +DELETE FROM level_progression WHERE level > 25; + +WITH RECURSIVE levels(level) AS ( + SELECT 1 + UNION ALL + SELECT level + 1 FROM levels WHERE level < 25 +) +INSERT OR IGNORE INTO level_progression (level, experience_required, talent_points_total) +SELECT level, (level - 1) * (level - 1) * 100, level +FROM levels; + +UPDATE characters +SET + level = MIN(level, 25), + experience = MIN( + experience, + (SELECT experience_required FROM level_progression WHERE level = 25) + ), + talent_points = MIN(talent_points, 25); + +UPDATE character_ability_slots +SET spell_id = NULL +WHERE spell_id IN ( + SELECT spells.id + FROM spells + JOIN characters ON characters.id = character_ability_slots.character_id + WHERE spells.unlock_level > characters.level +); + +-- Generated monster-part loot tiers. Dungeon 1 remains authored by hand above. +CREATE TEMP TABLE IF NOT EXISTS generated_loot_tiers ( + item_level INTEGER PRIMARY KEY, + dungeon_id INTEGER NOT NULL, + raid_id INTEGER NOT NULL, + dungeon_difficulty_id INTEGER NOT NULL, + raid_difficulty_id INTEGER NOT NULL, + recipe_base INTEGER NOT NULL, + craft_quantity INTEGER NOT NULL +); + +DELETE FROM generated_loot_tiers; +INSERT INTO generated_loot_tiers + (item_level, dungeon_id, raid_id, dungeon_difficulty_id, raid_difficulty_id, recipe_base, craft_quantity) +VALUES + (10, 3, 2, 2, 101, 1100, 2), + (15, 4, 5, 3, 103, 1200, 3), + (20, 6, 7, 4, 104, 1300, 4), + (25, 8, 9, 5, 105, 1400, 5); + +CREATE TEMP TABLE IF NOT EXISTS generated_bosses ( + item_level INTEGER NOT NULL, + boss_index INTEGER NOT NULL, + boss_global_index INTEGER NOT NULL, + boss_name TEXT NOT NULL, + boss_slug TEXT NOT NULL, + PRIMARY KEY (item_level, boss_index) +); + +DELETE FROM generated_bosses; +INSERT INTO generated_bosses + (item_level, boss_index, boss_global_index, boss_name, boss_slug) +VALUES + (10, 0, 0, 'Tigrex', 'tigrex'), + (10, 1, 1, 'Rathalos', 'rathalos'), + (10, 2, 2, 'Gypceros', 'gypceros'), + (15, 0, 3, 'Nargacuga', 'nargacuga'), + (15, 1, 4, 'Azuros', 'azuros'), + (15, 2, 5, 'Diablos', 'diablos'), + (20, 0, 6, 'Barroth', 'barroth'), + (20, 1, 7, 'Tobi Kadachi', 'tobi-kadachi'), + (20, 2, 8, 'Monoblos', 'monoblos'), + (25, 0, 9, 'Anjanath', 'anjanath'), + (25, 1, 10, 'Bazelgeuse', 'bazelgeuse'), + (25, 2, 11, 'Odogaron', 'odogaron'); + +CREATE TEMP TABLE IF NOT EXISTS generated_drop_patterns ( + boss_index INTEGER NOT NULL, + drop_index INTEGER NOT NULL, + drop_weight INTEGER NOT NULL, + rarity TEXT NOT NULL, + PRIMARY KEY (boss_index, drop_index) +); + +DELETE FROM generated_drop_patterns; +INSERT INTO generated_drop_patterns + (boss_index, drop_index, drop_weight, rarity) +VALUES + (0, 1, 50, 'uncommon'), + (0, 2, 5, 'rare'), + (0, 3, 15, 'uncommon'), + (0, 4, 30, 'uncommon'), + (1, 1, 32, 'uncommon'), + (1, 2, 50, 'uncommon'), + (1, 3, 13, 'uncommon'), + (1, 4, 5, 'rare'), + (2, 1, 36, 'uncommon'), + (2, 2, 52, 'uncommon'), + (2, 3, 10, 'rare'), + (2, 4, 2, 'epic'); + +INSERT OR IGNORE INTO locations (id, slug, name, description) VALUES + (3, 'monster-frontier', 'The Monster Frontier', 'Hunting grounds used for tiered monster-part progression.'); + +UPDATE dungeons +SET slug = 'tigrex-raid', + name = 'Tigrex Raid', + location_id = 3, + recommended_level = 5, + content_type = 'raid', + party_size = 10, + completion_item_level = NULL, + experience_reward = 275, + description = 'A raid-scale hunt against Tigrex, Rathalos, and Gypceros.' +WHERE id = 2; + +INSERT OR IGNORE INTO dungeons + (id, location_id, slug, name, recommended_level, content_type, party_size, completion_item_level, experience_reward, description) +VALUES + (3, 3, 'tigrex-hunting-ground', 'Tigrex Hunting Ground', 5, 'dungeon', 5, NULL, 205, 'A three-boss hunt featuring Tigrex, Rathalos, and Gypceros.'), + (4, 3, 'nargacuga-hunting-ground', 'Nargacuga Hunting Ground', 10, 'dungeon', 5, NULL, 245, 'A three-boss hunt featuring Nargacuga, Azuros, and Diablos.'), + (5, 3, 'nargacuga-raid', 'Nargacuga Raid', 10, 'raid', 10, NULL, 325, 'A raid-scale hunt against Nargacuga, Azuros, and Diablos.'), + (6, 3, 'barroth-hunting-ground', 'Barroth Hunting Ground', 15, 'dungeon', 5, NULL, 285, 'A three-boss hunt featuring Barroth, Tobi Kadachi, and Monoblos.'), + (7, 3, 'barroth-raid', 'Barroth Raid', 15, 'raid', 10, NULL, 375, 'A raid-scale hunt against Barroth, Tobi Kadachi, and Monoblos.'), + (8, 3, 'anjanath-hunting-ground', 'Anjanath Hunting Ground', 20, 'dungeon', 5, NULL, 325, 'A three-boss hunt featuring Anjanath, Bazelgeuse, and Odogaron.'), + (9, 3, 'anjanath-raid', 'Anjanath Raid', 20, 'raid', 10, NULL, 425, 'A raid-scale hunt against Anjanath, Bazelgeuse, and Odogaron.'); + +UPDATE difficulties +SET dropped_item_level = 10, + unlock_level = 5, + health_multiplier = 1.35, + damage_multiplier = 1.2, + experience_multiplier = 1.75, + description = 'Veteran raid difficulty with extra monster-part drops.' +WHERE id = 101; + +INSERT OR IGNORE INTO difficulties + (id, slug, name, dropped_item_level, unlock_level, health_multiplier, damage_multiplier, experience_multiplier, description) +VALUES + (103, 'raid-champion', 'Champion Raid', 15, 10, 1.7, 1.45, 2.4, 'Champion raid difficulty with extra monster-part drops.'), + (104, 'raid-mythic', 'Mythic Raid', 20, 15, 2.1, 1.75, 3.2, 'Mythic raid difficulty with extra monster-part drops.'), + (105, 'raid-ascendant', 'Ascendant Raid', 25, 20, 2.6, 2.1, 4.2, 'Ascendant raid difficulty with extra monster-part drops.'); + +DELETE FROM dungeon_difficulties WHERE dungeon_id = 2 AND difficulty_id <> 101; + +INSERT OR IGNORE INTO dungeon_difficulties (dungeon_id, difficulty_id) +SELECT dungeon_id, dungeon_difficulty_id FROM generated_loot_tiers +UNION ALL +SELECT raid_id, raid_difficulty_id FROM generated_loot_tiers; + +UPDATE encounters +SET slug = CASE id + WHEN 100 THEN 'tigrex-raid-approach' + WHEN 101 THEN 'tigrex-raid-guardians' + WHEN 102 THEN 'tigrex-raid' + WHEN 103 THEN 'rathalos-raid-approach' + WHEN 104 THEN 'rathalos-raid-guardians' + WHEN 105 THEN 'rathalos-raid' + WHEN 106 THEN 'gypceros-raid-approach' + WHEN 107 THEN 'gypceros-raid-guardians' + WHEN 108 THEN 'gypceros-raid' + ELSE slug + END, + name = CASE id + WHEN 100 THEN 'Tigrex Approach' + WHEN 101 THEN 'Tigrex Guardians' + WHEN 102 THEN 'Tigrex' + WHEN 103 THEN 'Rathalos Approach' + WHEN 104 THEN 'Rathalos Guardians' + WHEN 105 THEN 'Rathalos' + WHEN 106 THEN 'Gypceros Approach' + WHEN 107 THEN 'Gypceros Guardians' + WHEN 108 THEN 'Gypceros' + ELSE name + END, + encounter_type = CASE WHEN id IN (102, 105, 108) THEN 'boss' ELSE 'trash' END, + description = CASE id + WHEN 102 THEN 'Tigrex drops monster parts for item level 10 crafting.' + WHEN 105 THEN 'Rathalos drops monster parts for item level 10 crafting.' + WHEN 108 THEN 'Gypceros drops monster parts for item level 10 crafting.' + ELSE 'Hunters clear the raid path.' + END +WHERE id BETWEEN 100 AND 108; + +INSERT OR IGNORE INTO encounters + (id, dungeon_id, sequence, slug, name, encounter_type, max_health, base_damage, tank_damage, party_damage, description) +SELECT + generated_loot_tiers.dungeon_id * 100 + generated_bosses.boss_index * 3 + offset.value, + generated_loot_tiers.dungeon_id, + generated_bosses.boss_index * 3 + offset.value, + generated_bosses.boss_slug || '-dungeon-' || offset.slug, + CASE offset.encounter_type + WHEN 'boss' THEN generated_bosses.boss_name + ELSE generated_bosses.boss_name || ' ' || offset.name + END, + offset.encounter_type, + offset.health + generated_loot_tiers.item_level * 45 + generated_bosses.boss_index * 120, + offset.damage + generated_bosses.boss_index * 2 + generated_loot_tiers.item_level / 5, + offset.tank_damage + generated_bosses.boss_index * 2 + generated_loot_tiers.item_level / 8, + offset.party_damage + generated_bosses.boss_index * 3, + CASE offset.encounter_type + WHEN 'boss' THEN generated_bosses.boss_name || ' drops monster parts for item level ' || generated_loot_tiers.item_level || ' crafting.' + ELSE 'Hunters clear the path before ' || generated_bosses.boss_name || '.' + END +FROM generated_loot_tiers +JOIN generated_bosses ON generated_bosses.item_level = generated_loot_tiers.item_level +JOIN ( + SELECT 1 AS value, 'approach' AS slug, 'Approach' AS name, 'trash' AS encounter_type, 650 AS health, 15 AS damage, 9 AS tank_damage, 28 AS party_damage + UNION ALL SELECT 2, 'guardians', 'Guardians', 'trash', 720, 16, 10, 30 + UNION ALL SELECT 3, 'boss', '', 'boss', 980, 20, 14, 34 +) AS offset; + +INSERT OR IGNORE INTO encounters + (id, dungeon_id, sequence, slug, name, encounter_type, max_health, base_damage, tank_damage, party_damage, description) +SELECT + generated_loot_tiers.raid_id * 100 + generated_bosses.boss_index * 3 + offset.value, + generated_loot_tiers.raid_id, + generated_bosses.boss_index * 3 + offset.value, + generated_bosses.boss_slug || '-raid-' || offset.slug, + CASE offset.encounter_type + WHEN 'boss' THEN generated_bosses.boss_name + ELSE generated_bosses.boss_name || ' ' || offset.name + END, + offset.encounter_type, + offset.health + generated_loot_tiers.item_level * 45 + generated_bosses.boss_index * 120 + 900, + offset.damage + generated_bosses.boss_index * 2 + generated_loot_tiers.item_level / 5, + offset.tank_damage + generated_bosses.boss_index * 2 + generated_loot_tiers.item_level / 8, + offset.party_damage + generated_bosses.boss_index * 3 + 24, + CASE offset.encounter_type + WHEN 'boss' THEN generated_bosses.boss_name || ' drops monster parts for item level ' || generated_loot_tiers.item_level || ' crafting.' + ELSE 'Hunters clear the raid path before ' || generated_bosses.boss_name || '.' + END +FROM generated_loot_tiers +JOIN generated_bosses ON generated_bosses.item_level = generated_loot_tiers.item_level +JOIN ( + SELECT 1 AS value, 'approach' AS slug, 'Approach' AS name, 'trash' AS encounter_type, 650 AS health, 15 AS damage, 9 AS tank_damage, 28 AS party_damage + UNION ALL SELECT 2, 'guardians', 'Guardians', 'trash', 720, 16, 10, 30 + UNION ALL SELECT 3, 'boss', '', 'boss', 980, 20, 14, 34 +) AS offset +WHERE generated_loot_tiers.raid_id <> 2; + +INSERT OR IGNORE INTO items + (id, slug, name, slot, rarity, item_level, healing_power, max_resource_bonus, glyph, description) +SELECT + 3000 + generated_loot_tiers.item_level * 10 + generated_bosses.boss_global_index * 10 + generated_drop_patterns.drop_index, + generated_bosses.boss_slug || '-drop-' || generated_drop_patterns.drop_index || '-ilvl-' || generated_loot_tiers.item_level, + generated_bosses.boss_name || ' Drop ' || generated_drop_patterns.drop_index, + 'component', + generated_drop_patterns.rarity, + generated_loot_tiers.item_level, + 0, + 0, + CAST(generated_drop_patterns.drop_index AS TEXT), + 'A monster part from ' || generated_bosses.boss_name || '.' +FROM generated_loot_tiers +JOIN generated_bosses ON generated_bosses.item_level = generated_loot_tiers.item_level +JOIN generated_drop_patterns ON generated_drop_patterns.boss_index = generated_bosses.boss_index; + +DELETE FROM encounter_loot WHERE encounter_id NOT IN (3, 12, 22); +DELETE FROM encounter_loot WHERE item_id IN (SELECT id FROM items WHERE slot <> 'component'); + +INSERT OR IGNORE INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) +SELECT + generated_loot_tiers.dungeon_id * 100 + generated_bosses.boss_index * 3 + 3, + 3000 + generated_loot_tiers.item_level * 10 + generated_bosses.boss_global_index * 10 + generated_drop_patterns.drop_index, + generated_loot_tiers.dungeon_difficulty_id, + generated_drop_patterns.drop_weight, + 1.0 +FROM generated_loot_tiers +JOIN generated_bosses ON generated_bosses.item_level = generated_loot_tiers.item_level +JOIN generated_drop_patterns ON generated_drop_patterns.boss_index = generated_bosses.boss_index; + +INSERT OR IGNORE INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) +SELECT + CASE generated_loot_tiers.raid_id + WHEN 2 THEN 102 + generated_bosses.boss_index * 3 + ELSE generated_loot_tiers.raid_id * 100 + generated_bosses.boss_index * 3 + 3 + END, + 3000 + generated_loot_tiers.item_level * 10 + generated_bosses.boss_global_index * 10 + generated_drop_patterns.drop_index, + generated_loot_tiers.raid_difficulty_id, + generated_drop_patterns.drop_weight, + 1.0 +FROM generated_loot_tiers +JOIN generated_bosses ON generated_bosses.item_level = generated_loot_tiers.item_level +JOIN generated_drop_patterns ON generated_drop_patterns.boss_index = generated_bosses.boss_index; + +CREATE TEMP TABLE IF NOT EXISTS generated_recipe_offsets ( + recipe_offset INTEGER PRIMARY KEY, + boss_index INTEGER NOT NULL +); + +DELETE FROM generated_recipe_offsets; +INSERT INTO generated_recipe_offsets (recipe_offset, boss_index) VALUES + (1, 0), (2, 0), (3, 0), + (4, 1), (5, 1), (6, 1), + (7, 2), (8, 2), (9, 2); + +UPDATE crafting_recipes +SET source_dungeon_id = ( + SELECT generated_loot_tiers.dungeon_id + FROM generated_loot_tiers + WHERE crafting_recipes.id BETWEEN generated_loot_tiers.recipe_base + 1 AND generated_loot_tiers.recipe_base + 9 + ), + source_encounter_id = ( + SELECT generated_loot_tiers.dungeon_id * 100 + generated_recipe_offsets.boss_index * 3 + 3 + FROM generated_loot_tiers + JOIN generated_recipe_offsets + ON crafting_recipes.id = generated_loot_tiers.recipe_base + generated_recipe_offsets.recipe_offset + ) +WHERE id BETWEEN 1101 AND 1409; + +UPDATE crafting_recipes +SET source_dungeon_id = 2, + source_encounter_id = 102 + ( + SELECT generated_recipe_offsets.boss_index * 3 + FROM generated_recipe_offsets + WHERE crafting_recipes.id = 2000 + generated_recipe_offsets.recipe_offset + ) +WHERE id BETWEEN 2001 AND 2009; + +DELETE FROM crafting_recipe_components +WHERE recipe_id BETWEEN 1101 AND 1409 + OR recipe_id BETWEEN 2001 AND 2009; + +INSERT OR IGNORE INTO crafting_recipe_components (recipe_id, item_id, quantity) +SELECT + generated_loot_tiers.recipe_base + generated_recipe_offsets.recipe_offset, + 3000 + generated_loot_tiers.item_level * 10 + generated_bosses.boss_global_index * 10 + component.drop_index, + CASE component.drop_index + WHEN 1 THEN 5 + WHEN 2 THEN 3 + WHEN 4 THEN 1 + END +FROM generated_loot_tiers +JOIN generated_recipe_offsets +JOIN generated_bosses + ON generated_bosses.item_level = generated_loot_tiers.item_level + AND generated_bosses.boss_index = generated_recipe_offsets.boss_index +JOIN ( + SELECT 1 AS drop_index + UNION ALL SELECT 2 + UNION ALL SELECT 4 +) AS component; + +INSERT OR IGNORE INTO crafting_recipe_components (recipe_id, item_id, quantity) +SELECT + 2000 + generated_recipe_offsets.recipe_offset, + 3000 + generated_loot_tiers.item_level * 10 + generated_bosses.boss_global_index * 10 + component.drop_index, + CASE component.drop_index + WHEN 1 THEN 5 + WHEN 2 THEN 3 + WHEN 4 THEN 1 + END +FROM generated_loot_tiers +JOIN generated_recipe_offsets +JOIN generated_bosses + ON generated_bosses.item_level = generated_loot_tiers.item_level + AND generated_bosses.boss_index = generated_recipe_offsets.boss_index +JOIN ( + SELECT 1 AS drop_index + UNION ALL SELECT 2 + UNION ALL SELECT 4 +) AS component +WHERE generated_loot_tiers.item_level = 10; + +UPDATE items +SET name = CASE id + WHEN 5 THEN 'Bulldrome Helmet' + WHEN 2 THEN 'Bulldrome Chest' + WHEN 6 THEN 'Bulldrome Gloves' + WHEN 4 THEN 'Yian Kut-Ku Boots' + WHEN 1 THEN 'Yian Kut-Ku Ring' + WHEN 7 THEN 'Yian Kut-Ku Trinket' + WHEN 3 THEN 'Rathian Weapon' + WHEN 8 THEN 'Rathian Pants' + WHEN 9 THEN 'Rathian Necklace' + WHEN 205 THEN 'Tempered Tigrex Helmet' + WHEN 202 THEN 'Tempered Tigrex Chest' + WHEN 206 THEN 'Tempered Tigrex Gloves' + WHEN 204 THEN 'Tempered Rathalos Boots' + WHEN 201 THEN 'Tempered Rathalos Ring' + WHEN 207 THEN 'Tempered Rathalos Trinket' + WHEN 203 THEN 'Tempered Gypceros Weapon' + WHEN 208 THEN 'Tempered Gypceros Pants' + WHEN 209 THEN 'Tempered Gypceros Necklace' + WHEN 305 THEN 'Runed Nargacuga Helmet' + WHEN 302 THEN 'Runed Nargacuga Chest' + WHEN 306 THEN 'Runed Nargacuga Gloves' + WHEN 304 THEN 'Runed Azuros Boots' + WHEN 301 THEN 'Runed Azuros Ring' + WHEN 307 THEN 'Runed Azuros Trinket' + WHEN 303 THEN 'Runed Diablos Weapon' + WHEN 308 THEN 'Runed Diablos Pants' + WHEN 309 THEN 'Runed Diablos Necklace' + WHEN 405 THEN 'Mythic Barroth Helmet' + WHEN 402 THEN 'Mythic Barroth Chest' + WHEN 406 THEN 'Mythic Barroth Gloves' + WHEN 404 THEN 'Mythic Tobi Kadachi Boots' + WHEN 401 THEN 'Mythic Tobi Kadachi Ring' + WHEN 407 THEN 'Mythic Tobi Kadachi Trinket' + WHEN 403 THEN 'Mythic Monoblos Weapon' + WHEN 408 THEN 'Mythic Monoblos Pants' + WHEN 409 THEN 'Mythic Monoblos Necklace' + WHEN 505 THEN 'Ascendant Anjanath Helmet' + WHEN 502 THEN 'Ascendant Anjanath Chest' + WHEN 506 THEN 'Ascendant Anjanath Gloves' + WHEN 504 THEN 'Ascendant Bazelgeuse Boots' + WHEN 501 THEN 'Ascendant Bazelgeuse Ring' + WHEN 507 THEN 'Ascendant Bazelgeuse Trinket' + WHEN 503 THEN 'Ascendant Odogaron Weapon' + WHEN 508 THEN 'Ascendant Odogaron Pants' + WHEN 509 THEN 'Ascendant Odogaron Necklace' + WHEN 714 THEN 'Raid Tigrex Helmet' + WHEN 711 THEN 'Raid Tigrex Chest' + WHEN 715 THEN 'Raid Tigrex Gloves' + WHEN 713 THEN 'Raid Rathalos Boots' + WHEN 710 THEN 'Raid Rathalos Ring' + WHEN 716 THEN 'Raid Rathalos Trinket' + WHEN 712 THEN 'Raid Gypceros Weapon' + WHEN 717 THEN 'Raid Gypceros Pants' + WHEN 718 THEN 'Raid Gypceros Necklace' + ELSE name + END, + description = CASE id + WHEN 5 THEN 'Crafted from Bulldrome monster parts.' + WHEN 2 THEN 'Crafted from Bulldrome monster parts.' + WHEN 6 THEN 'Crafted from Bulldrome monster parts.' + WHEN 4 THEN 'Crafted from Yian Kut-Ku monster parts.' + WHEN 1 THEN 'Crafted from Yian Kut-Ku monster parts.' + WHEN 7 THEN 'Crafted from Yian Kut-Ku monster parts.' + WHEN 3 THEN 'Crafted from Rathian monster parts.' + WHEN 8 THEN 'Crafted from Rathian monster parts.' + WHEN 9 THEN 'Crafted from Rathian monster parts.' + ELSE description + END +WHERE id IN ( + 1, 2, 3, 4, 5, 6, 7, 8, 9, + 201, 202, 203, 204, 205, 206, 207, 208, 209, + 301, 302, 303, 304, 305, 306, 307, 308, 309, + 401, 402, 403, 404, 405, 406, 407, 408, 409, + 501, 502, 503, 504, 505, 506, 507, 508, 509, + 710, 711, 712, 713, 714, 715, 716, 717, 718 +); + +UPDATE items +SET slug = CASE id + WHEN 860 THEN 'bulldrome-drop-1' + WHEN 861 THEN 'bulldrome-drop-2' + WHEN 862 THEN 'bulldrome-drop-3' + WHEN 863 THEN 'bulldrome-drop-4' + WHEN 864 THEN 'yian-kut-ku-drop-1' + WHEN 865 THEN 'yian-kut-ku-drop-2' + WHEN 866 THEN 'yian-kut-ku-drop-3' + WHEN 867 THEN 'yian-kut-ku-drop-4' + WHEN 868 THEN 'rathian-drop-1' + WHEN 869 THEN 'rathian-drop-2' + WHEN 870 THEN 'rathian-drop-3' + WHEN 871 THEN 'rathian-drop-4' + ELSE slug + END, + name = CASE id + WHEN 860 THEN 'Bulldrome Drop 1' + WHEN 861 THEN 'Bulldrome Drop 2' + WHEN 862 THEN 'Bulldrome Drop 3' + WHEN 863 THEN 'Bulldrome Drop 4' + WHEN 864 THEN 'Yian Kut-Ku Drop 1' + WHEN 865 THEN 'Yian Kut-Ku Drop 2' + WHEN 866 THEN 'Yian Kut-Ku Drop 3' + WHEN 867 THEN 'Yian Kut-Ku Drop 4' + WHEN 868 THEN 'Rathian Drop 1' + WHEN 869 THEN 'Rathian Drop 2' + WHEN 870 THEN 'Rathian Drop 3' + WHEN 871 THEN 'Rathian Drop 4' + ELSE name + END, + glyph = CASE id + WHEN 860 THEN '1' + WHEN 861 THEN '2' + WHEN 862 THEN '3' + WHEN 863 THEN '4' + WHEN 864 THEN '1' + WHEN 865 THEN '2' + WHEN 866 THEN '3' + WHEN 867 THEN '4' + WHEN 868 THEN '1' + WHEN 869 THEN '2' + WHEN 870 THEN '3' + WHEN 871 THEN '4' + ELSE glyph + END, + description = CASE + WHEN id BETWEEN 860 AND 863 THEN 'A monster part from Bulldrome.' + WHEN id BETWEEN 864 AND 867 THEN 'A monster part from Yian Kut-Ku.' + WHEN id BETWEEN 868 AND 871 THEN 'A monster part from Rathian.' + ELSE description + END +WHERE id BETWEEN 860 AND 871; + +DELETE FROM crafting_recipe_components WHERE recipe_id BETWEEN 1001 AND 1009; + +INSERT OR IGNORE INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES + (1001, 860, 5), (1001, 861, 3), (1001, 863, 1), + (1002, 860, 5), (1002, 861, 3), (1002, 863, 1), + (1003, 860, 5), (1003, 861, 3), (1003, 863, 1), + (1004, 864, 5), (1004, 865, 3), (1004, 867, 1), + (1005, 864, 5), (1005, 865, 3), (1005, 867, 1), + (1006, 864, 5), (1006, 865, 3), (1006, 867, 1), + (1007, 868, 5), (1007, 869, 3), (1007, 871, 1), + (1008, 868, 5), (1008, 869, 3), (1008, 871, 1), + (1009, 868, 5), (1009, 869, 3), (1009, 871, 1); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c4eb64b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist', 'android/app/build', 'android/build']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..6cb642e --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + testgame + + +

+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..57ef71f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3730 @@ +{ + "name": "ashen-halls", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ashen-halls", + "version": "0.0.0", + "dependencies": { + "@capacitor/android": "^8.4.0", + "@capacitor/core": "^8.4.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@capacitor/cli": "^8.4.0", + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.0.tgz", + "integrity": "sha512-K1ZPkQzvRzPEALz9nBdLx5p5nAPzp5fsTYWk7LRiKZeH/NXqjDvqfTv7lrLgrziQNoDeaL6ijg64oBREzXiV+g==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.4.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.4.0.tgz", + "integrity": "sha512-5Z9RKHxiqJYRTLrfMeZmzR4qrlg5B85MxsWZ5goyXsLkO3bgpW9a1qV/6fR1SX9s5gwLza5y7PZVwITl/hDJ7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/core": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.0.tgz", + "integrity": "sha512-LrS1xPIrqLtJABBIPDGXxxKmI9OyesrzWw8DiHbxhSC9JoiLUleUAJlX1a0LWIVLRbuY4Szgf9huFeRqYH2SAQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..94cdb0d --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "ashen-halls", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "predev": "npm run db:init", + "dev": "vite", + "build": "npx tsc -b --noEmit && npx vite build && node scripts/generate-service-worker.mjs", + "android:sync": "npm run build && cap sync android", + "android:open": "cap open android", + "android:apk": "npm run android:sync && cd android && ./gradlew assembleDebug", + "accounts:ip": "node scripts/manage-ip-allowance.mjs", + "db:backup": "node scripts/backup-db.mjs", + "db:init": "node scripts/init-db.mjs", + "offline:export": "node scripts/export-offline-profile.mjs", + "lint": "eslint .", + "admin:start": "node server/admin.mjs", + "start": "node server/production.mjs", + "prepreview": "npm run db:init", + "preview": "vite preview" + }, + "dependencies": { + "@capacitor/android": "^8.4.0", + "@capacitor/core": "^8.4.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@capacitor/cli": "^8.4.0", + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/public/boss-placeholder.svg b/public/boss-placeholder.svg new file mode 100644 index 0000000..10a685e --- /dev/null +++ b/public/boss-placeholder.svg @@ -0,0 +1,10 @@ + + Boss placeholder + A placeholder boss icon with a monster silhouette. + + + + + + + diff --git a/public/equipment-placeholder.svg b/public/equipment-placeholder.svg new file mode 100644 index 0000000..faff5ac --- /dev/null +++ b/public/equipment-placeholder.svg @@ -0,0 +1,9 @@ + + Equipment placeholder + A placeholder equipment icon with an item silhouette. + + + + + + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/backup-db.mjs b/scripts/backup-db.mjs new file mode 100644 index 0000000..784b5fe --- /dev/null +++ b/scripts/backup-db.mjs @@ -0,0 +1,20 @@ +import { mkdirSync } from 'node:fs' +import { resolve } from 'node:path' +import { DatabaseSync } from 'node:sqlite' + +const sourcePath = resolve('data/game.db') +const backupDirectory = resolve('backups') +const timestamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-') +const backupPath = resolve(backupDirectory, `game-${timestamp}.db`) + +mkdirSync(backupDirectory, { recursive: true }) + +const database = new DatabaseSync(sourcePath) + +try { + const escapedPath = backupPath.replaceAll("'", "''") + database.exec(`VACUUM INTO '${escapedPath}'`) + console.log(`SQLite backup created: ${backupPath}`) +} finally { + database.close() +} diff --git a/scripts/export-offline-profile.mjs b/scripts/export-offline-profile.mjs new file mode 100644 index 0000000..8ca48ef --- /dev/null +++ b/scripts/export-offline-profile.mjs @@ -0,0 +1,18 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { DatabaseSync } from 'node:sqlite' +import { getProfile } from '../server/game-api.mjs' + +const database = new DatabaseSync(':memory:') + +try { + database.exec(readFileSync('db/schema.sql', 'utf8')) + database.exec(readFileSync('db/seed.sql', 'utf8')) + const profile = getProfile(database, 1) + writeFileSync( + 'src/offline-starter-profile.json', + `${JSON.stringify(profile, null, 2)}\n`, + ) + console.log('Offline starter profile exported from SQLite.') +} finally { + database.close() +} diff --git a/scripts/generate-service-worker.mjs b/scripts/generate-service-worker.mjs new file mode 100644 index 0000000..2dc03a5 --- /dev/null +++ b/scripts/generate-service-worker.mjs @@ -0,0 +1,63 @@ +import { readdirSync, writeFileSync } from 'node:fs' +import { relative, resolve } from 'node:path' + +const distDirectory = resolve('dist') + +function listFiles(directory) { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = resolve(directory, entry.name) + return entry.isDirectory() ? listFiles(path) : [path] + }) +} + +const assets = listFiles(distDirectory) + .map((path) => `/${relative(distDirectory, path).replaceAll('\\', '/')}`) + .filter((path) => path !== '/service-worker.js') + .sort() +const cacheName = `chronicle-${Date.now()}` +const source = `const CACHE_NAME = ${JSON.stringify(cacheName)} +const APP_ASSETS = ${JSON.stringify(assets, null, 2)} + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => cache.addAll(APP_ASSETS)) + .then(() => self.skipWaiting()), + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((keys) => Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)), + )) + .then(() => self.clients.claim()), + ) +}) + +self.addEventListener('fetch', (event) => { + const requestUrl = new URL(event.request.url) + if ( + event.request.method !== 'GET' + || requestUrl.origin !== self.location.origin + || requestUrl.pathname.startsWith('/api/') + ) { + return + } + + event.respondWith( + caches.match(event.request).then((cached) => { + if (cached) return cached + return fetch(event.request).catch(() => ( + event.request.mode === 'navigate' + ? caches.match('/index.html') + : Response.error() + )) + }), + ) +}) +` + +writeFileSync(resolve(distDirectory, 'service-worker.js'), source) +console.log(`Offline app shell generated with ${assets.length} files.`) diff --git a/scripts/init-db.mjs b/scripts/init-db.mjs new file mode 100644 index 0000000..a904261 --- /dev/null +++ b/scripts/init-db.mjs @@ -0,0 +1,262 @@ +import { mkdirSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { DatabaseSync } from 'node:sqlite' + +mkdirSync('data', { recursive: true }) + +const database = new DatabaseSync('data/game.db') +const schema = await readFile(new URL('../db/schema.sql', import.meta.url), 'utf8') +const seed = await readFile(new URL('../db/seed.sql', import.meta.url), 'utf8') + +database.exec(schema) + +database.exec(` + CREATE TABLE IF NOT EXISTS crafting_recipes ( + id INTEGER PRIMARY KEY, + item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, + difficulty_id INTEGER REFERENCES difficulties(id), + source_dungeon_id INTEGER REFERENCES dungeons(id) ON DELETE CASCADE, + source_encounter_id INTEGER REFERENCES encounters(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS crafting_recipe_components ( + recipe_id INTEGER NOT NULL REFERENCES crafting_recipes(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + quantity INTEGER NOT NULL CHECK (quantity > 0), + PRIMARY KEY (recipe_id, item_id) + ); + + CREATE TABLE IF NOT EXISTS item_sets ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS item_set_items ( + set_id INTEGER NOT NULL REFERENCES item_sets(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, + PRIMARY KEY (set_id, item_id) + ); + + CREATE TABLE IF NOT EXISTS item_set_bonuses ( + id INTEGER PRIMARY KEY, + set_id INTEGER NOT NULL REFERENCES item_sets(id) ON DELETE CASCADE, + required_pieces INTEGER NOT NULL CHECK (required_pieces > 0), + effect_type TEXT NOT NULL, + description TEXT NOT NULL, + UNIQUE (set_id, required_pieces) + ); + + CREATE TABLE IF NOT EXISTS encounter_loot_roll_items ( + roll_id INTEGER NOT NULL REFERENCES encounter_loot_rolls(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id), + quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0), + was_duplicate INTEGER NOT NULL DEFAULT 0 CHECK (was_duplicate IN (0, 1)), + quantity_after INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (roll_id, item_id) + ); +`) + +function addColumnIfMissing(table, column, definition) { + const columns = database.prepare(`PRAGMA table_info(${table})`).all() + if (!columns.some((candidate) => candidate.name === column)) { + database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`) + } +} + +function migrateCharacterAccountConstraint() { + const tableSql = database.prepare(` + SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'characters' + `).get()?.sql ?? '' + const hasLegacyAccountUnique = /account_id\s+INTEGER\s+UNIQUE/i.test(tableSql) + if (!hasLegacyAccountUnique) return + + const columns = database.prepare('PRAGMA table_info(characters)').all() + const hasCompletedDungeonParts = columns.some( + (candidate) => candidate.name === 'completed_dungeon_parts', + ) + + database.exec('PRAGMA foreign_keys = OFF') + database.exec('BEGIN') + try { + database.exec(` + CREATE TABLE characters_migrated ( + id INTEGER PRIMARY KEY, + account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, + class_id INTEGER NOT NULL REFERENCES classes(id), + name TEXT NOT NULL, + level INTEGER NOT NULL DEFAULT 1, + experience INTEGER NOT NULL DEFAULT 0, + talent_points INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (account_id, class_id) + ) + `) + database.exec(` + INSERT INTO characters_migrated + (id, account_id, class_id, name, level, experience, talent_points, created_at) + SELECT + id, account_id, class_id, name, level, experience, talent_points, created_at + FROM characters + `) + if (hasCompletedDungeonParts) { + database.exec(` + UPDATE accounts + SET completed_dungeon_parts = MAX( + completed_dungeon_parts, + COALESCE(( + SELECT MAX(completed_dungeon_parts) + FROM characters + WHERE characters.account_id = accounts.id + ), 0) + ) + `) + } + database.exec('DROP TABLE characters') + database.exec('ALTER TABLE characters_migrated RENAME TO characters') + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } finally { + database.exec('PRAGMA foreign_keys = ON') + } +} + +function migrateItemSlotConstraint() { + const tableSql = database.prepare(` + SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'items' + `).get()?.sql ?? '' + if (tableSql.includes("'pants'") && tableSql.includes("'necklace'")) return + + database.exec('PRAGMA foreign_keys = OFF') + database.exec('BEGIN') + try { + database.exec(` + CREATE TABLE items_migrated ( + id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + slot TEXT NOT NULL CHECK (slot IN ('weapon', 'helmet', 'chest', 'gloves', 'boots', 'pants', 'ring', 'necklace', 'trinket', 'component')), + rarity TEXT NOT NULL, + item_level INTEGER NOT NULL, + healing_power INTEGER NOT NULL DEFAULT 0, + max_resource_bonus INTEGER NOT NULL DEFAULT 0, + glyph TEXT NOT NULL DEFAULT '?', + image_url TEXT NOT NULL DEFAULT '/equipment-placeholder.svg', + description TEXT NOT NULL + ) + `) + database.exec(` + INSERT INTO items_migrated + (id, slug, name, slot, rarity, item_level, healing_power, max_resource_bonus, glyph, image_url, description) + SELECT id, slug, name, slot, rarity, item_level, healing_power, max_resource_bonus, glyph, image_url, description + FROM items + `) + database.exec('DROP TABLE items') + database.exec('ALTER TABLE items_migrated RENAME TO items') + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } finally { + database.exec('PRAGMA foreign_keys = ON') + } +} + +function migrateEncounterLootPrimaryKey() { + const tableSql = database.prepare(` + SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'encounter_loot' + `).get()?.sql ?? '' + if (/PRIMARY KEY\s*\(\s*encounter_id\s*,\s*difficulty_id\s*,\s*item_id\s*\)/i.test(tableSql)) return + + database.exec('PRAGMA foreign_keys = OFF') + database.exec('BEGIN') + try { + database.exec(` + CREATE TABLE encounter_loot_migrated ( + encounter_id INTEGER NOT NULL REFERENCES encounters(id) ON DELETE CASCADE, + item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE, + difficulty_id INTEGER REFERENCES difficulties(id), + drop_weight INTEGER NOT NULL DEFAULT 100, + drop_chance REAL NOT NULL DEFAULT 0.65 CHECK (drop_chance BETWEEN 0 AND 1), + PRIMARY KEY (encounter_id, difficulty_id, item_id) + ) + `) + database.exec(` + INSERT OR IGNORE INTO encounter_loot_migrated + (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) + SELECT encounter_id, item_id, difficulty_id, drop_weight, drop_chance + FROM encounter_loot + `) + database.exec('DROP TABLE encounter_loot') + database.exec('ALTER TABLE encounter_loot_migrated RENAME TO encounter_loot') + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } finally { + database.exec('PRAGMA foreign_keys = ON') + } +} + +addColumnIfMissing('dungeons', 'content_type', "TEXT NOT NULL DEFAULT 'dungeon'") +addColumnIfMissing('dungeons', 'party_size', 'INTEGER NOT NULL DEFAULT 5') +addColumnIfMissing('dungeons', 'completion_item_level', 'INTEGER') +addColumnIfMissing('dungeons', 'experience_reward', 'INTEGER NOT NULL DEFAULT 100') +addColumnIfMissing('classes', 'theme_color', "TEXT NOT NULL DEFAULT '#e5b95f'") +addColumnIfMissing('spells', 'unlock_level', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('spells', 'glyph', "TEXT NOT NULL DEFAULT '+'") +addColumnIfMissing('encounter_loot', 'difficulty_id', 'INTEGER REFERENCES difficulties(id)') +addColumnIfMissing('characters', 'talent_points', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('characters', 'account_id', 'INTEGER REFERENCES accounts(id)') +addColumnIfMissing('characters', 'completed_dungeon_parts', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('talents', 'branch', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('talents', 'prerequisite_talent_id', 'INTEGER REFERENCES talents(id)') +addColumnIfMissing('talents', 'prerequisite_rank', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('talents', 'effect_type', "TEXT NOT NULL DEFAULT 'placeholder'") +addColumnIfMissing('talents', 'effect_value_per_rank', 'REAL NOT NULL DEFAULT 0') +addColumnIfMissing('talents', 'glyph', "TEXT NOT NULL DEFAULT '+'") +addColumnIfMissing('items', 'glyph', "TEXT NOT NULL DEFAULT '?'") +addColumnIfMissing('items', 'image_url', "TEXT NOT NULL DEFAULT '/equipment-placeholder.svg'") +addColumnIfMissing('difficulties', 'unlock_level', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('difficulties', 'health_multiplier', 'REAL NOT NULL DEFAULT 1') +addColumnIfMissing('difficulties', 'damage_multiplier', 'REAL NOT NULL DEFAULT 1') +addColumnIfMissing('difficulties', 'experience_multiplier', 'REAL NOT NULL DEFAULT 1') +addColumnIfMissing('dungeon_runs', 'difficulty_id', 'INTEGER REFERENCES difficulties(id)') +addColumnIfMissing('dungeon_runs', 'character_name', "TEXT NOT NULL DEFAULT ''") +addColumnIfMissing('dungeon_runs', 'class_name', "TEXT NOT NULL DEFAULT ''") +addColumnIfMissing('dungeon_runs', 'character_level', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('dungeon_runs', 'average_item_level', 'REAL NOT NULL DEFAULT 1') +addColumnIfMissing('dungeon_runs', 'resource_spent', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('dungeon_runs', 'duration_seconds', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('dungeon_runs', 'leaderboard_eligible', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('dungeon_runs', 'start_part', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('dungeon_runs', 'completed_parts', 'INTEGER NOT NULL DEFAULT 1') +addColumnIfMissing('encounter_loot', 'drop_chance', 'REAL NOT NULL DEFAULT 0.65') +addColumnIfMissing('encounters', 'image_url', "TEXT NOT NULL DEFAULT '/boss-placeholder.svg'") + +addColumnIfMissing('accounts', 'completed_dungeon_parts', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('accounts', 'completed_raid_phases', 'INTEGER NOT NULL DEFAULT 0') +addColumnIfMissing('sessions', 'active_character_id', 'INTEGER REFERENCES characters(id)') + +migrateCharacterAccountConstraint() +migrateItemSlotConstraint() +migrateEncounterLootPrimaryKey() + +addColumnIfMissing('items', 'image_url', "TEXT NOT NULL DEFAULT '/equipment-placeholder.svg'") + +database.exec(seed) + +const counts = database + .prepare(` + SELECT + (SELECT COUNT(*) FROM classes) AS classes, + (SELECT COUNT(*) FROM encounters) AS encounters, + (SELECT COUNT(*) FROM items) AS items + `) + .get() + +database.close() +console.log(`Database ready: ${counts.classes} class, ${counts.encounters} encounters, ${counts.items} items.`) diff --git a/scripts/manage-ip-allowance.mjs b/scripts/manage-ip-allowance.mjs new file mode 100644 index 0000000..9cc9ad3 --- /dev/null +++ b/scripts/manage-ip-allowance.mjs @@ -0,0 +1,78 @@ +import { isIP } from 'node:net' +import { DatabaseSync } from 'node:sqlite' + +const [command, rawIp, rawLimit, ...noteParts] = process.argv.slice(2) +const database = new DatabaseSync('data/game.db') + +function normalizeIp(value) { + const address = String(value ?? '').trim() + if (address.startsWith('::ffff:') && isIP(address.slice(7)) === 4) { + return address.slice(7) + } + return address +} + +function requireIp(value) { + const address = normalizeIp(value) + if (!isIP(address)) { + throw new Error('Provide a valid IPv4 or IPv6 address.') + } + return address +} + +try { + if (command === 'set') { + const ip = requireIp(rawIp) + const maxAccounts = Number(rawLimit) + if (!Number.isInteger(maxAccounts) || maxAccounts < 2 || maxAccounts > 100) { + throw new Error('The allowed account count must be an integer from 2 to 100.') + } + const note = noteParts.join(' ').slice(0, 200) + database.prepare(` + INSERT INTO account_ip_allowances + (ip_address, max_accounts, note, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(ip_address) DO UPDATE SET + max_accounts = excluded.max_accounts, + note = excluded.note, + updated_at = CURRENT_TIMESTAMP + `).run(ip, maxAccounts, note) + console.log(`${ip} may create up to ${maxAccounts} accounts${note ? ` (${note})` : ''}.`) + } else if (command === 'remove') { + const ip = requireIp(rawIp) + const result = database.prepare(` + DELETE FROM account_ip_allowances WHERE ip_address = ? + `).run(ip) + console.log( + result.changes + ? `${ip} returned to the default one-account limit.` + : `${ip} had no custom allowance.`, + ) + } else if (command === 'list') { + const rows = database.prepare(` + SELECT + account_ip_allowances.ip_address AS ip, + account_ip_allowances.max_accounts AS maxAccounts, + account_ip_allowances.note, + account_ip_allowances.updated_at AS updatedAt, + COUNT(accounts.id) AS existingAccounts + FROM account_ip_allowances + LEFT JOIN accounts + ON accounts.created_ip = account_ip_allowances.ip_address + GROUP BY account_ip_allowances.ip_address + ORDER BY account_ip_allowances.updated_at DESC + `).all() + console.table(rows) + } else { + console.log('Usage:') + console.log(' npm run accounts:ip -- set [note]') + console.log(' npm run accounts:ip -- remove ') + console.log(' npm run accounts:ip -- list') + process.exitCode = command ? 1 : 0 + } +} catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 +} finally { + database.close() +} diff --git a/server/admin.mjs b/server/admin.mjs new file mode 100644 index 0000000..07946d7 --- /dev/null +++ b/server/admin.mjs @@ -0,0 +1,333 @@ +import { createReadStream, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs' +import { createServer } from 'node:http' +import { randomBytes } from 'node:crypto' +import { extname, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { DatabaseSync } from 'node:sqlite' + +const host = '127.0.0.1' +const port = Number(process.env.ADMIN_PORT ?? 4174) +const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url)) +const distPath = fileURLToPath(new URL('../dist', import.meta.url)) +const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url)) +const itemImageDirectory = fileURLToPath(new URL('../data/uploads/items/', import.meta.url)) + +const bossImageContentTypes = { + '.gif': 'image/gif', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', +} + +function sendJson(response, status, body) { + response.statusCode = status + response.setHeader('Content-Type', 'application/json') + response.end(JSON.stringify(body)) +} + +async function readJson(request, maxSize = 16 * 1024) { + const chunks = [] + let size = 0 + for await (const chunk of request) { + size += chunk.length + if (size > maxSize) { + const error = new Error('Request body is too large.') + error.status = 413 + throw error + } + chunks.push(chunk) + } + return JSON.parse(Buffer.concat(chunks).toString('utf8')) +} + +function sendBossImage(request, response) { + const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const filename = pathname.replace('/api/boss-images/', '') + if (!/^[A-Za-z0-9._-]+$/.test(filename)) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + const imagePath = resolve(bossImageDirectory, filename) + const insideDirectory = imagePath.startsWith(resolve(bossImageDirectory) + sep) + const extension = extname(imagePath).toLowerCase() + if (!insideDirectory || !bossImageContentTypes[extension] || !existsSync(imagePath) || !statSync(imagePath).isFile()) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + response.statusCode = 200 + response.setHeader('Content-Type', bossImageContentTypes[extension]) + response.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + response.setHeader('X-Content-Type-Options', 'nosniff') + createReadStream(imagePath).pipe(response) +} + +function sendItemImage(request, response) { + const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const filename = pathname.replace('/api/item-images/', '') + if (!/^[A-Za-z0-9._-]+$/.test(filename)) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + const imagePath = resolve(itemImageDirectory, filename) + const insideDirectory = imagePath.startsWith(resolve(itemImageDirectory) + sep) + const extension = extname(imagePath).toLowerCase() + if (!insideDirectory || !bossImageContentTypes[extension] || !existsSync(imagePath) || !statSync(imagePath).isFile()) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + response.statusCode = 200 + response.setHeader('Content-Type', bossImageContentTypes[extension]) + response.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + response.setHeader('X-Content-Type-Options', 'nosniff') + createReadStream(imagePath).pipe(response) +} + +function saveBossImage(database, encounterId, payload) { + const encounter = database.prepare(` + SELECT id, slug, encounter_type AS encounterType + FROM encounters WHERE id = ? + `).get(encounterId) + if (!encounter || encounter.encounterType !== 'boss') { + throw new Error('Boss encounter not found.') + } + const dataUrl = String(payload.imageData ?? '') + const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=]+)$/) + if (!match) throw new Error('Upload a PNG, JPG, WebP, or GIF image.') + const extensionByType = { 'image/gif': 'gif', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' } + const bytes = Buffer.from(match[2], 'base64') + if (bytes.length === 0 || bytes.length > 4 * 1024 * 1024) { + throw new Error('Boss image must be 1 byte to 4 MB.') + } + mkdirSync(bossImageDirectory, { recursive: true }) + const filename = `${encounter.slug}-${Date.now()}-${randomBytes(4).toString('hex')}.${extensionByType[match[1]]}` + writeFileSync(resolve(bossImageDirectory, filename), bytes, { mode: 0o644 }) + const imageUrl = `/api/boss-images/${filename}` + database.prepare(`UPDATE encounters SET image_url = ? WHERE id = ?`).run(imageUrl, encounterId) + return imageUrl +} + +function saveItemImage(database, itemId, payload) { + const item = database.prepare(`SELECT id, slug, slot FROM items WHERE id = ?`).get(itemId) + if (!item || item.slot === 'component') throw new Error('Equipment item not found.') + const dataUrl = String(payload.imageData ?? '') + const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=]+)$/) + if (!match) throw new Error('Upload a PNG, JPG, WebP, or GIF image.') + const extensionByType = { 'image/gif': 'gif', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' } + const bytes = Buffer.from(match[2], 'base64') + if (bytes.length === 0 || bytes.length > 4 * 1024 * 1024) { + throw new Error('Equipment image must be 1 byte to 4 MB.') + } + mkdirSync(itemImageDirectory, { recursive: true }) + const filename = `${item.slug}-${Date.now()}-${randomBytes(4).toString('hex')}.${extensionByType[match[1]]}` + writeFileSync(resolve(itemImageDirectory, filename), bytes, { mode: 0o644 }) + const imageUrl = `/api/item-images/${filename}` + database.prepare(`UPDATE items SET image_url = ? WHERE id = ?`).run(imageUrl, itemId) + return imageUrl +} + +function sendFile(response, filePath) { + const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + } + response.statusCode = 200 + response.setHeader('Content-Type', contentTypes[extname(filePath).toLowerCase()] ?? 'application/octet-stream') + response.setHeader('X-Content-Type-Options', 'nosniff') + response.setHeader('Referrer-Policy', 'same-origin') + response.setHeader('X-Frame-Options', 'DENY') + createReadStream(filePath).pipe(response) +} + +function serveStatic(request, response) { + const requestPath = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const candidate = resolve(distPath, `.${requestPath}`) + const insideDist = candidate === distPath || candidate.startsWith(`${distPath}${sep}`) + if (insideDist && existsSync(candidate) && statSync(candidate).isFile()) { + sendFile(response, candidate) + return + } + const adminIndexPath = resolve(distPath, 'admin.html') + if (!existsSync(adminIndexPath)) { + response.statusCode = 503 + response.end('Admin build missing. Run npm run build.') + return + } + sendFile(response, adminIndexPath) +} + +const server = createServer(async (request, response) => { + try { + if (!request.url?.startsWith('/api/')) { + serveStatic(request, response) + return + } + + if (request.url.startsWith('/api/boss-images/') && request.method === 'GET') { + sendBossImage(request, response) + return + } + + if (request.url.startsWith('/api/item-images/') && request.method === 'GET') { + sendItemImage(request, response) + return + } + + if (!existsSync(databasePath)) { + sendJson(response, 503, { error: 'Database missing. Run npm run db:init.' }) + return + } + + const database = new DatabaseSync(databasePath) + database.exec('PRAGMA foreign_keys = ON') + database.exec('PRAGMA journal_mode = WAL') + database.exec('PRAGMA busy_timeout = 5000') + + try { + if (request.url === '/api/admin/data' && request.method === 'GET') { + const items = database.prepare(` + SELECT id, slug, name, slot, rarity, item_level AS itemLevel, + healing_power AS healingPower, max_resource_bonus AS maxResourceBonus, + glyph, image_url AS imageUrl, description + FROM items ORDER BY slot, item_level, id + `).all() + const encounters = database.prepare(` + SELECT id, dungeon_id AS dungeonId, sequence, slug, name AS enemyName, + encounter_type AS encounterType, image_url AS imageUrl + FROM encounters ORDER BY dungeon_id, sequence + `).all() + const difficulties = database.prepare(` + SELECT id, slug, name, dropped_item_level AS droppedItemLevel + FROM difficulties ORDER BY id + `).all() + const encounterLoot = database.prepare(` + SELECT encounter_id AS encounterId, item_id AS itemId, + difficulty_id AS difficultyId, drop_weight AS dropWeight, drop_chance AS dropChance + FROM encounter_loot ORDER BY encounter_id, difficulty_id, item_id + `).all() + const craftingRecipes = database.prepare(` + SELECT cr.id, cr.item_id AS itemId, cr.difficulty_id AS difficultyId, + cr.source_dungeon_id AS sourceDungeonId, + cr.source_encounter_id AS sourceEncounterId, + crc.item_id AS componentId, crc.quantity + FROM crafting_recipes cr + LEFT JOIN crafting_recipe_components crc ON crc.recipe_id = cr.id + ORDER BY cr.id, crc.item_id + `).all() + const recipes = new Map() + for (const row of craftingRecipes) { + if (!recipes.has(row.id)) { + recipes.set(row.id, { + id: row.id, itemId: row.itemId, difficultyId: row.difficultyId, + sourceDungeonId: row.sourceDungeonId, sourceEncounterId: row.sourceEncounterId, + components: [], + }) + } + if (row.componentId) { + recipes.get(row.id).components.push({ itemId: row.componentId, quantity: row.quantity }) + } + } + const dungeons = database.prepare(`SELECT id, slug, name FROM dungeons ORDER BY id`).all() + sendJson(response, 200, { items, encounters, difficulties, encounterLoot, craftingRecipes: [...recipes.values()], dungeons }) + return + } + + const bossImageMatch = request.url.match(/^\/api\/admin\/encounters\/(\d+)\/image$/) + if (bossImageMatch && request.method === 'PUT') { + const payload = await readJson(request, 6 * 1024 * 1024) + const imageUrl = saveBossImage(database, Number(bossImageMatch[1]), payload) + sendJson(response, 200, { ok: true, imageUrl }) + return + } + + const itemImageMatch = request.url.match(/^\/api\/admin\/items\/(\d+)\/image$/) + if (itemImageMatch && request.method === 'PUT') { + const payload = await readJson(request, 6 * 1024 * 1024) + const imageUrl = saveItemImage(database, Number(itemImageMatch[1]), payload) + sendJson(response, 200, { ok: true, imageUrl }) + return + } + + const itemMatch = request.url.match(/^\/api\/admin\/items\/(\d+)$/) + if (itemMatch && request.method === 'PUT') { + const payload = await readJson(request) + const itemId = Number(itemMatch[1]) + const fields = [] + const values = [] + for (const key of ['name', 'slug', 'slot', 'rarity', 'glyph', 'description']) { + if (payload[key] !== undefined) { fields.push(`${key} = ?`); values.push(payload[key]) } + } + for (const key of ['item_level', 'healing_power', 'max_resource_bonus']) { + if (payload[key] !== undefined) { fields.push(`${key} = ?`); values.push(Number(payload[key])) } + } + if (fields.length === 0) { sendJson(response, 400, { error: 'No fields to update.' }); return } + values.push(itemId) + database.prepare(`UPDATE items SET ${fields.join(', ')} WHERE id = ?`).run(...values) + sendJson(response, 200, { ok: true }) + return + } + + if (request.url === '/api/admin/encounter-loot' && request.method === 'POST') { + const payload = await readJson(request) + database.prepare(` + INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(encounter_id, difficulty_id, item_id) + DO UPDATE SET drop_weight = excluded.drop_weight, drop_chance = excluded.drop_chance + `).run(payload.encounterId, payload.itemId, payload.difficultyId, payload.dropWeight ?? 100, payload.dropChance ?? 0.65) + sendJson(response, 200, { ok: true }) + return + } + + const lootDelete = request.url.match(/^\/api\/admin\/encounter-loot\/(\d+)\/(\d+)\/(\d+)$/) + if (lootDelete && request.method === 'DELETE') { + database.prepare(`DELETE FROM encounter_loot WHERE encounter_id = ? AND difficulty_id = ? AND item_id = ?`) + .run(Number(lootDelete[1]), Number(lootDelete[2]), Number(lootDelete[3])) + sendJson(response, 200, { ok: true }) + return + } + + const recipeComponents = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components$/) + if (recipeComponents && request.method === 'POST') { + const payload = await readJson(request) + database.prepare(` + INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) + VALUES (?, ?, ?) + ON CONFLICT(recipe_id, item_id) + DO UPDATE SET quantity = excluded.quantity + `).run(Number(recipeComponents[1]), payload.itemId, payload.quantity) + sendJson(response, 200, { ok: true }) + return + } + + const recipeComponentDelete = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components\/(\d+)$/) + if (recipeComponentDelete && request.method === 'DELETE') { + database.prepare(`DELETE FROM crafting_recipe_components WHERE recipe_id = ? AND item_id = ?`) + .run(Number(recipeComponentDelete[1]), Number(recipeComponentDelete[2])) + sendJson(response, 200, { ok: true }) + return + } + + sendJson(response, 404, { error: 'Admin API route not found.' }) + } catch (error) { + const status = Number(error?.status) || 400 + sendJson(response, status, { error: error instanceof Error ? error.message : 'Unable to process request.' }) + } finally { + database.close() + } + } catch (error) { + sendJson(response, 500, { error: error instanceof Error ? error.message : 'Server error.' }) + } +}) + +server.listen(port, host, () => { + console.log(`Admin panel listening on http://${host}:${port}`) +}) diff --git a/server/game-api.d.mts b/server/game-api.d.mts new file mode 100644 index 0000000..e786c57 --- /dev/null +++ b/server/game-api.d.mts @@ -0,0 +1,3 @@ +import type { Plugin } from 'vite' + +export function gameApiPlugin(): Plugin diff --git a/server/game-api.mjs b/server/game-api.mjs new file mode 100644 index 0000000..d2b3822 --- /dev/null +++ b/server/game-api.mjs @@ -0,0 +1,2094 @@ +import { createReadStream, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { + createHash, + randomBytes, + scryptSync, + timingSafeEqual, +} from 'node:crypto' +import { isIP } from 'node:net' +import { extname, resolve, sep } from 'node:path' +import { DatabaseSync } from 'node:sqlite' + +const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url)) +const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url)) +const itemImageDirectory = fileURLToPath(new URL('../data/uploads/items/', import.meta.url)) +const bossImageContentTypes = { + '.gif': 'image/gif', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', +} +const equipmentSlots = ['weapon', 'helmet', 'chest', 'gloves', 'boots', 'pants', 'ring', 'necklace', 'trinket'] +const componentSlot = 'component' +const sessionCookieName = 'chronicle_session' +const sessionLifetimeSeconds = 60 * 60 * 24 * 30 +const rateLimitBuckets = new Map() + +function sendJson(response, status, body, headers = {}) { + response.statusCode = status + response.setHeader('Content-Type', 'application/json') + for (const [name, value] of Object.entries(headers)) response.setHeader(name, value) + response.end(JSON.stringify(body)) +} + +async function readJson(request, maxSize = 16 * 1024) { + const chunks = [] + let size = 0 + for await (const chunk of request) { + size += chunk.length + if (size > maxSize) { + const error = new Error('Request body is too large.') + error.status = 413 + throw error + } + chunks.push(chunk) + } + return JSON.parse(Buffer.concat(chunks).toString('utf8')) +} + +function sendBossImage(request, response) { + const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const filename = pathname.replace('/api/boss-images/', '') + if (!/^[A-Za-z0-9._-]+$/.test(filename)) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + const imagePath = resolve(bossImageDirectory, filename) + const insideImageDirectory = imagePath.startsWith(resolve(bossImageDirectory) + sep) + const extension = extname(imagePath).toLowerCase() + if ( + !insideImageDirectory + || !bossImageContentTypes[extension] + || !existsSync(imagePath) + || !statSync(imagePath).isFile() + ) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + response.statusCode = 200 + response.setHeader('Content-Type', bossImageContentTypes[extension]) + response.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + response.setHeader('X-Content-Type-Options', 'nosniff') + createReadStream(imagePath).pipe(response) +} + +function sendItemImage(request, response) { + const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const filename = pathname.replace('/api/item-images/', '') + if (!/^[A-Za-z0-9._-]+$/.test(filename)) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + const imagePath = resolve(itemImageDirectory, filename) + const insideImageDirectory = imagePath.startsWith(resolve(itemImageDirectory) + sep) + const extension = extname(imagePath).toLowerCase() + if ( + !insideImageDirectory + || !bossImageContentTypes[extension] + || !existsSync(imagePath) + || !statSync(imagePath).isFile() + ) { + sendJson(response, 404, { error: 'Image not found.' }) + return + } + response.statusCode = 200 + response.setHeader('Content-Type', bossImageContentTypes[extension]) + response.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + response.setHeader('X-Content-Type-Options', 'nosniff') + createReadStream(imagePath).pipe(response) +} + +function saveBossImage(database, encounterId, payload) { + const encounter = database.prepare(` + SELECT id, slug, encounter_type AS encounterType + FROM encounters + WHERE id = ? + `).get(encounterId) + if (!encounter || encounter.encounterType !== 'boss') { + throw new Error('Boss encounter not found.') + } + const dataUrl = String(payload.imageData ?? '') + const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=]+)$/) + if (!match) { + throw new Error('Upload a PNG, JPG, WebP, or GIF image.') + } + const extensionByType = { + 'image/gif': 'gif', + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + } + const bytes = Buffer.from(match[2], 'base64') + if (bytes.length === 0 || bytes.length > 4 * 1024 * 1024) { + throw new Error('Boss image must be 1 byte to 4 MB.') + } + mkdirSync(bossImageDirectory, { recursive: true }) + const filename = `${encounter.slug}-${Date.now()}-${randomBytes(4).toString('hex')}.${extensionByType[match[1]]}` + writeFileSync(resolve(bossImageDirectory, filename), bytes, { mode: 0o644 }) + const imageUrl = `/api/boss-images/${filename}` + database.prepare(` + UPDATE encounters + SET image_url = ? + WHERE id = ? + `).run(imageUrl, encounterId) + return imageUrl +} + +function saveItemImage(database, itemId, payload) { + const item = database.prepare(` + SELECT id, slug, slot + FROM items + WHERE id = ? + `).get(itemId) + if (!item || item.slot === componentSlot) { + throw new Error('Equipment item not found.') + } + const dataUrl = String(payload.imageData ?? '') + const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=]+)$/) + if (!match) { + throw new Error('Upload a PNG, JPG, WebP, or GIF image.') + } + const extensionByType = { + 'image/gif': 'gif', + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + } + const bytes = Buffer.from(match[2], 'base64') + if (bytes.length === 0 || bytes.length > 4 * 1024 * 1024) { + throw new Error('Equipment image must be 1 byte to 4 MB.') + } + mkdirSync(itemImageDirectory, { recursive: true }) + const filename = `${item.slug}-${Date.now()}-${randomBytes(4).toString('hex')}.${extensionByType[match[1]]}` + writeFileSync(resolve(itemImageDirectory, filename), bytes, { mode: 0o644 }) + const imageUrl = `/api/item-images/${filename}` + database.prepare(` + UPDATE items + SET image_url = ? + WHERE id = ? + `).run(imageUrl, itemId) + return imageUrl +} + +function normalizeIp(value) { + const address = String(value ?? '').trim() + if (address.startsWith('::ffff:') && isIP(address.slice(7)) === 4) { + return address.slice(7) + } + return address +} + +function requestIp(request) { + if (process.env.TRUST_PROXY === '1') { + const forwarded = request.headers['x-forwarded-for'] + if (typeof forwarded === 'string') { + return normalizeIp(forwarded.split(',')[0]) + } + } + return normalizeIp(request.socket.remoteAddress ?? 'unknown') +} + +function consumeRateLimit(key, limit, windowMs) { + const now = Date.now() + const bucket = rateLimitBuckets.get(key) + if (!bucket || now >= bucket.resetAt) { + rateLimitBuckets.set(key, { count: 1, resetAt: now + windowMs }) + return + } + bucket.count += 1 + if (bucket.count > limit) { + const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)) + const error = new Error(`Too many requests. Try again in ${retryAfter} seconds.`) + error.status = 429 + error.retryAfter = retryAfter + throw error + } +} + +function normalizeUsername(value) { + const username = String(value ?? '').trim() + if (!/^[A-Za-z0-9_]{3,20}$/.test(username)) { + throw new Error('Username must be 3-20 letters, numbers, or underscores.') + } + return username +} + +function normalizeCharacterName(value, fallback) { + const name = String(value ?? fallback).trim() + if (!/^[A-Za-z][A-Za-z0-9 '-]{1,19}$/.test(name)) { + throw new Error('Character name must be 2-20 characters and start with a letter.') + } + return name +} + +function validatePassword(value) { + const password = String(value ?? '') + if (password.length < 10 || password.length > 128) { + throw new Error('Password must be 10-128 characters.') + } + return password +} + +function passwordDigest(password, salt) { + return scryptSync(password, salt, 64).toString('hex') +} + +function verifyPassword(password, account) { + const actual = Buffer.from(passwordDigest(password, account.passwordSalt), 'hex') + const expected = Buffer.from(account.passwordHash, 'hex') + return actual.length === expected.length && timingSafeEqual(actual, expected) +} + +function tokenHash(token) { + return createHash('sha256').update(token).digest('hex') +} + +function parseCookies(request) { + return Object.fromEntries( + String(request.headers.cookie ?? '') + .split(';') + .map((part) => part.trim()) + .filter(Boolean) + .map((part) => { + const separator = part.indexOf('=') + return separator < 0 + ? [part, ''] + : [part.slice(0, separator), decodeURIComponent(part.slice(separator + 1))] + }), + ) +} + +function sessionCookie(token, request, maxAge = sessionLifetimeSeconds) { + const secure = request.headers['x-forwarded-proto'] === 'https' + || Boolean(request.socket.encrypted) + || process.env.COOKIE_SECURE === '1' + return [ + `${sessionCookieName}=${encodeURIComponent(token)}`, + 'HttpOnly', + 'Path=/', + 'SameSite=Lax', + `Max-Age=${maxAge}`, + secure ? 'Secure' : '', + ].filter(Boolean).join('; ') +} + +function createSession(database, accountId, ip, activeCharacterId) { + const token = randomBytes(32).toString('base64url') + database.prepare(` + INSERT INTO sessions (account_id, token_hash, active_character_id, expires_at, created_ip) + VALUES (?, ?, ?, datetime('now', '+30 days'), ?) + `).run(accountId, tokenHash(token), activeCharacterId ?? null, ip) + return token +} + +function currentSession(database, request) { + const token = parseCookies(request)[sessionCookieName] + if (!token) return null + return database.prepare(` + SELECT + sessions.id AS sessionId, + accounts.id AS accountId, + accounts.username, + characters.id AS characterId, + characters.class_id AS classId + FROM sessions + JOIN accounts ON accounts.id = sessions.account_id + JOIN characters ON characters.id = sessions.active_character_id + WHERE sessions.token_hash = ? + AND sessions.expires_at > CURRENT_TIMESTAMP + `).get(tokenHash(token)) ?? null +} + +function requireSession(database, request) { + const session = currentSession(database, request) + if (!session) { + const error = new Error('Sign in to continue.') + error.status = 401 + throw error + } + return session +} + +function initializeCharacter(database, accountId, characterName, classId) { + const result = database.prepare(` + INSERT INTO characters (account_id, class_id, name, level, experience, talent_points) + VALUES (?, ?, ?, 1, 0, 1) + `).run(accountId, classId, characterName) + const characterId = Number(result.lastInsertRowid) + const insertSlot = database.prepare(` + INSERT INTO character_ability_slots (character_id, slot_number, spell_id) + VALUES (?, ?, ?) + `) + const starterSpells = database.prepare(` + SELECT id FROM spells WHERE class_id = ? AND unlock_level = 1 ORDER BY id + `).all(classId).map((s) => s.id) + ;[...starterSpells, null].slice(0, 6).forEach((spellId, index) => { + insertSlot.run(characterId, index + 1, spellId) + }) + const insertItem = database.prepare(` + INSERT INTO character_inventory (character_id, item_id, quantity, equipped) + VALUES (?, ?, 1, ?) + `) + for (let itemId = 100; itemId <= 107; itemId += 1) { + insertItem.run(characterId, itemId, itemId === 107 ? 0 : 1) + } + return characterId +} + +function registerAccount(database, request, payload) { + const ip = requestIp(request) + consumeRateLimit(`register:${ip}`, 3, 60 * 60 * 1000) + const username = normalizeUsername(payload.username) + const password = validatePassword(payload.password) + const characterName = normalizeCharacterName(payload.characterName, username) + const existing = database.prepare(` + SELECT id FROM accounts WHERE username = ? COLLATE NOCASE + `).get(username) + if (existing) throw new Error('That username is already taken.') + const accountLimit = database.prepare(` + SELECT max_accounts AS maxAccounts + FROM account_ip_allowances + WHERE ip_address = ? + `).get(ip)?.maxAccounts ?? 1 + const existingIpAccounts = database.prepare(` + SELECT COUNT(*) AS count FROM accounts WHERE created_ip = ? + `).get(ip).count + if (existingIpAccounts >= accountLimit) { + throw new Error( + 'This IP address has reached its account limit. Contact the server administrator for an exception.', + ) + } + + const salt = randomBytes(16).toString('hex') + database.exec('BEGIN') + try { + const accountResult = database.prepare(` + INSERT INTO accounts (username, password_hash, password_salt, created_ip) + VALUES (?, ?, ?, ?) + `).run(username, passwordDigest(password, salt), salt, ip) + const accountId = Number(accountResult.lastInsertRowid) + const activeCharacterId = initializeCharacter(database, accountId, characterName, 1) + initializeCharacter(database, accountId, characterName, 2) + initializeCharacter(database, accountId, characterName, 3) + const token = createSession(database, accountId, ip, activeCharacterId) + database.exec('COMMIT') + return { + token, + account: { id: accountId, username }, + profile: getProfile(database, activeCharacterId, accountId), + } + } catch (error) { + database.exec('ROLLBACK') + throw error + } +} + +function loginAccount(database, request, payload) { + const ip = requestIp(request) + consumeRateLimit(`login:${ip}`, 8, 15 * 60 * 1000) + const username = normalizeUsername(payload.username) + const password = String(payload.password ?? '') + const account = database.prepare(` + SELECT + accounts.id, + accounts.username, + accounts.password_hash AS passwordHash, + accounts.password_salt AS passwordSalt + FROM accounts + WHERE accounts.username = ? COLLATE NOCASE + `).get(username) + if (!account || !verifyPassword(password, account)) { + throw new Error('Invalid username or password.') + } + const activeCharacterId = database.prepare(` + SELECT id FROM characters WHERE account_id = ? ORDER BY class_id LIMIT 1 + `).get(account.id)?.id + if (!activeCharacterId) throw new Error('No character found for this account.') + const token = createSession(database, account.id, ip, activeCharacterId) + return { + token, + account: { id: account.id, username: account.username }, + profile: getProfile(database, activeCharacterId, account.id), + } +} + +export function getProfile(database, characterId, accountId) { + if (!accountId) { + const row = database.prepare('SELECT account_id FROM characters WHERE id = ?').get(characterId) + accountId = row?.account_id ?? null + } + const character = database.prepare(` + SELECT + characters.id, + characters.name, + characters.level, + characters.experience, + characters.talent_points AS talentPoints, + classes.id AS classId, + classes.slug AS classSlug, + classes.name AS className, + classes.resource_name AS resourceName, + classes.max_resource AS maxResource, + classes.theme_color AS themeColor, + classes.description AS classDescription + FROM characters + JOIN classes ON classes.id = characters.class_id + WHERE characters.id = ? + `).get(characterId) + + const classes = database.prepare(` + SELECT + id, + slug, + name, + resource_name AS resourceName, + max_resource AS maxResource, + theme_color AS themeColor, + description + FROM classes + ORDER BY id + `).all() + + const spells = database.prepare(` + SELECT + id, + class_id AS classId, + slug, + name, + spell_type AS spellType, + resource_cost AS cost, + cooldown_seconds AS cooldown, + power, + unlock_level AS unlockLevel, + glyph, + description + FROM spells + ORDER BY class_id, unlock_level, id + `).all() + + const slots = database.prepare(` + SELECT slot_number AS slotNumber, spell_id AS spellId + FROM character_ability_slots + WHERE character_id = ? + ORDER BY slot_number + `).all(characterId) + const talents = database.prepare(` + SELECT + talents.id, + talents.class_id AS classId, + talents.slug, + talents.name, + talents.max_rank AS maxRank, + talents.tier, + talents.branch, + talents.prerequisite_talent_id AS prerequisiteTalentId, + talents.prerequisite_rank AS prerequisiteRank, + prerequisite.name AS prerequisiteName, + talents.effect_type AS effectType, + talents.effect_value_per_rank AS effectValuePerRank, + talents.glyph, + talents.description, + COALESCE(character_talents.rank, 0) AS rank + FROM talents + LEFT JOIN talents AS prerequisite + ON prerequisite.id = talents.prerequisite_talent_id + LEFT JOIN character_talents + ON character_talents.talent_id = talents.id + AND character_talents.character_id = ? + ORDER BY talents.class_id, talents.tier, talents.branch + `).all(characterId) + const inventory = database.prepare(` + SELECT + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName, + character_inventory.quantity, + character_inventory.equipped + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + WHERE character_inventory.character_id = ? + ORDER BY items.slot, items.item_level DESC, items.id + `).all(characterId).map((item) => ({ ...item, equipped: Boolean(item.equipped) })) + const equippedItems = inventory.filter((item) => item.equipped) + const gearStats = { + averageItemLevel: equipmentSlots.length === 0 + ? 0 + : equippedItems.reduce((total, item) => total + item.itemLevel, 0) / equipmentSlots.length, + healingPower: equippedItems.reduce((total, item) => total + item.healingPower, 0), + maxResourceBonus: equippedItems.reduce((total, item) => total + item.maxResourceBonus, 0), + } + const dungeons = database.prepare(` + SELECT + dungeons.id, + dungeons.slug, + dungeons.name, + dungeons.recommended_level AS recommendedLevel, + dungeons.content_type AS contentType, + dungeons.party_size AS partySize, + dungeons.completion_item_level AS completionItemLevel, + dungeons.experience_reward AS experienceReward, + dungeons.description, + locations.name AS locationName + FROM dungeons + JOIN locations ON locations.id = dungeons.location_id + ORDER BY dungeons.id + `).all() + const dungeonDifficulties = database.prepare(` + SELECT + dungeon_difficulties.dungeon_id AS dungeonId, + difficulties.id, + difficulties.slug, + difficulties.name, + difficulties.dropped_item_level AS droppedItemLevel, + difficulties.unlock_level AS unlockLevel, + difficulties.health_multiplier AS healthMultiplier, + difficulties.damage_multiplier AS damageMultiplier, + difficulties.experience_multiplier AS experienceMultiplier, + difficulties.description + FROM dungeon_difficulties + JOIN difficulties ON difficulties.id = dungeon_difficulties.difficulty_id + ORDER BY dungeon_difficulties.dungeon_id, difficulties.id + `).all() + const encounters = database.prepare(` + SELECT + id, + dungeon_id AS dungeonId, + sequence, + slug, + name AS enemyName, + encounter_type AS encounterType, + max_health AS maxHealth, + base_damage AS damage, + tank_damage AS tankDamage, + party_damage AS partyDamage, + description, + image_url AS imageUrl + FROM encounters + ORDER BY dungeon_id, sequence + `).all() + const encounterLoot = database.prepare(` + SELECT + encounter_loot.encounter_id AS encounterId, + encounter_loot.difficulty_id AS difficultyId, + encounter_loot.drop_weight AS dropWeight, + encounter_loot.drop_chance AS dropChance, + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName + FROM encounter_loot + JOIN items ON items.id = encounter_loot.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + ORDER BY encounter_loot.encounter_id, encounter_loot.difficulty_id, items.slot + `).all() + const completionLoot = database.prepare(` + SELECT + dungeon_completion_loot.dungeon_id AS dungeonId, + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName + FROM dungeon_completion_loot + JOIN items ON items.id = dungeon_completion_loot.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + ORDER BY dungeon_completion_loot.dungeon_id, items.slot + `).all() + const craftingRecipeRows = database.prepare(` + SELECT + crafting_recipes.id, + crafting_recipes.difficulty_id AS difficultyId, + crafting_recipes.source_dungeon_id AS sourceDungeonId, + crafting_recipes.source_encounter_id AS sourceEncounterId, + output.id AS itemId, + output.slug, + output.name, + output.slot, + output.rarity, + output.item_level AS itemLevel, + output.healing_power AS healingPower, + output.max_resource_bonus AS maxResourceBonus, + output.glyph, + output.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName + FROM crafting_recipes + JOIN items AS output ON output.id = crafting_recipes.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = output.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + ORDER BY crafting_recipes.source_dungeon_id, crafting_recipes.difficulty_id, output.item_level, output.slot + `).all() + const craftingComponentRows = database.prepare(` + SELECT + crafting_recipe_components.recipe_id AS recipeId, + crafting_recipe_components.quantity, + components.id, + components.slug, + components.name, + components.slot, + components.rarity, + components.item_level AS itemLevel, + components.healing_power AS healingPower, + components.max_resource_bonus AS maxResourceBonus, + components.glyph, + components.description, + COALESCE(character_inventory.quantity, 0) AS owned + FROM crafting_recipe_components + JOIN items AS components ON components.id = crafting_recipe_components.item_id + LEFT JOIN character_inventory + ON character_inventory.item_id = components.id + AND character_inventory.character_id = ? + ORDER BY crafting_recipe_components.recipe_id, components.id + `).all(characterId) + const setBonuses = database.prepare(` + SELECT + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName, + item_set_bonuses.required_pieces AS requiredPieces, + item_set_bonuses.effect_type AS effectType, + item_set_bonuses.description, + COALESCE(equipped_sets.equippedPieces, 0) AS equippedPieces + FROM item_set_bonuses + JOIN item_sets ON item_sets.id = item_set_bonuses.set_id + LEFT JOIN ( + SELECT item_set_items.set_id AS setId, COUNT(*) AS equippedPieces + FROM item_set_items + JOIN character_inventory + ON character_inventory.item_id = item_set_items.item_id + AND character_inventory.character_id = ? + AND character_inventory.equipped = 1 + GROUP BY item_set_items.set_id + ) AS equipped_sets ON equipped_sets.setId = item_sets.id + ORDER BY item_sets.id, item_set_bonuses.required_pieces + `).all(characterId).map((bonus) => ({ + ...bonus, + active: bonus.equippedPieces >= bonus.requiredPieces, + })) + const leaderboardRuns = database.prepare(` + SELECT + rank, + dungeonId, + difficultyId, + startPart, + completedParts, + characterName, + className, + characterLevel, + averageItemLevel, + resourceSpent, + durationSeconds, + completedAt + FROM ( + SELECT + ROW_NUMBER() OVER ( + PARTITION BY dungeon_id, difficulty_id, start_part, completed_parts + ORDER BY + resource_spent, + average_item_level, + duration_seconds, + completed_at + ) AS rank, + dungeon_id AS dungeonId, + difficulty_id AS difficultyId, + start_part AS startPart, + completed_parts AS completedParts, + character_name AS characterName, + class_name AS className, + character_level AS characterLevel, + average_item_level AS averageItemLevel, + resource_spent AS resourceSpent, + duration_seconds AS durationSeconds, + completed_at AS completedAt + FROM dungeon_runs + WHERE result = 'victory' + AND leaderboard_eligible = 1 + ) + WHERE rank <= 10 + ORDER BY dungeonId, difficultyId, startPart, completedParts, rank + `).all() + + const settings = Object.fromEntries( + database.prepare('SELECT key, value FROM game_settings').all() + .map((setting) => [setting.key, Number(setting.value)]), + ) + const currentLevel = database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(character.level) + const nextLevel = database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(character.level + 1) + + return { + character: { + ...character, + currentLevelExperience: currentLevel?.experienceRequired ?? character.experience, + nextLevelExperience: nextLevel?.experienceRequired ?? character.experience, + }, + classes: classes.map((gameClass) => ({ + ...gameClass, + spells: spells.filter((spell) => spell.classId === gameClass.id), + talents: talents.filter((talent) => talent.classId === gameClass.id), + })), + abilitySlots: Array.from({ length: 6 }, (_, index) => { + const slot = slots.find((candidate) => candidate.slotNumber === index + 1) + return slot?.spellId ?? null + }), + maxLevel: settings.max_level ?? 25, + maxTalentPoints: settings.max_talent_points ?? 25, + allocatedTalentPoints: talents.reduce((total, talent) => total + talent.rank, 0), + completedDungeonParts: accountId + ? (database.prepare('SELECT completed_dungeon_parts AS value FROM accounts WHERE id = ?').get(accountId)?.value ?? 0) + : 0, + completedRaidPhases: accountId + ? (database.prepare('SELECT completed_raid_phases AS value FROM accounts WHERE id = ?').get(accountId)?.value ?? 0) + : 0, + equipmentSlots, + inventory, + gearStats, + setBonuses, + craftingRecipes: craftingRecipeRows.map((recipe) => { + const components = craftingComponentRows + .filter((component) => component.recipeId === recipe.id) + .map(({ recipeId, owned, quantity, ...item }) => ({ + item, + quantity, + owned, + })) + const { itemId, slug, name, slot, rarity, itemLevel, healingPower, maxResourceBonus, glyph, description, setId, setSlug, setName } = recipe + return { + id: recipe.id, + difficultyId: recipe.difficultyId, + sourceDungeonId: recipe.sourceDungeonId, + sourceEncounterId: recipe.sourceEncounterId, + item: { + id: itemId, + slug, + name, + slot, + rarity, + itemLevel, + healingPower, + maxResourceBonus, + glyph, + description, + setId, + setSlug, + setName, + }, + components, + canCraft: components.every((component) => component.owned >= component.quantity), + } + }), + dungeons: dungeons.map((dungeon) => ({ + ...dungeon, + difficulties: dungeonDifficulties.filter( + (difficulty) => difficulty.dungeonId === dungeon.id, + ), + encounters: encounters + .filter((encounter) => encounter.dungeonId === dungeon.id) + .map((encounter) => ({ + ...encounter, + isBoss: encounter.encounterType === 'boss', + lootTables: encounterLoot.filter( + (entry) => entry.encounterId === encounter.id, + ), + })), + completionLoot: completionLoot.filter((item) => item.dungeonId === dungeon.id), + leaderboard: leaderboardRuns + .filter((run) => run.dungeonId === dungeon.id) + .map((run) => ({ ...run, rank: Number(run.rank) })), + leaderboards: { + part_1: leaderboardRuns + .filter((run) => run.dungeonId === dungeon.id && run.startPart === 1 && run.completedParts === 1) + .map((run) => ({ ...run, rank: Number(run.rank) })), + part_2: leaderboardRuns + .filter((run) => run.dungeonId === dungeon.id && run.startPart === 2 && run.completedParts === 1) + .map((run) => ({ ...run, rank: Number(run.rank) })), + part_3: leaderboardRuns + .filter((run) => run.dungeonId === dungeon.id && run.startPart === 3 && run.completedParts === 1) + .map((run) => ({ ...run, rank: Number(run.rank) })), + full_run: leaderboardRuns + .filter((run) => run.dungeonId === dungeon.id && run.startPart === 1 && run.completedParts === 3) + .map((run) => ({ ...run, rank: Number(run.rank) })), + }, + })), + } +} + +function itemById(database, itemId) { + return database.prepare(` + SELECT + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName + FROM items + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + WHERE items.id = ? + `).get(itemId) +} + +function formatLootRoll(database, context, record, dropChance) { + let items = [] + if (record.id) { + items = database.prepare(` + SELECT + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName, + encounter_loot_roll_items.quantity, + encounter_loot_roll_items.was_duplicate AS wasDuplicate, + encounter_loot_roll_items.quantity_after AS quantityAfter + FROM encounter_loot_roll_items + JOIN items ON items.id = encounter_loot_roll_items.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + WHERE encounter_loot_roll_items.roll_id = ? + ORDER BY items.id + `).all(record.id).map((item) => ({ + ...item, + duplicate: Boolean(item.wasDuplicate), + })) + } + if (items.length === 0 && record.itemId) { + const item = itemById(database, record.itemId) + if (item) { + items = [{ + ...item, + quantity: 1, + duplicate: Boolean(record.wasDuplicate), + quantityAfter: record.quantityAfter, + }] + } + } + const item = items[0] ?? null + + return { + encounterId: context.id, + encounterName: context.encounterName, + difficultyId: context.difficultyId, + difficultyName: context.difficultyName, + dropChance, + dropped: Boolean(record.dropped), + item, + items, + awarded: Boolean(record.dropped), + duplicate: items.some((candidate) => candidate.duplicate) || Boolean(record.wasDuplicate), + quantityAfter: item?.quantityAfter ?? record.quantityAfter, + } +} + +function componentDropQuantity(droppedItemLevel) { + const tier = Math.max(0, Math.floor((droppedItemLevel - 5) / 5)) + const secondChance = Math.min(0.85, 0.35 + tier * 0.12) + const thirdChance = Math.min(0.6, 0.1 + tier * 0.1) + return 1 + (Math.random() < secondChance ? 1 : 0) + (Math.random() < thirdChance ? 1 : 0) +} + +function rollWeightedLootEntry(entries) { + const totalWeight = entries.reduce((total, entry) => total + entry.dropWeight, 0) + let weightedRoll = Math.random() * totalWeight + for (const entry of entries) { + weightedRoll -= entry.dropWeight + if (weightedRoll < 0) return entry + } + return entries[entries.length - 1] +} + +function rollEncounterLoot(database, characterId, encounterId, difficultyId, runToken) { + if (typeof runToken !== 'string' || runToken.length < 8 || runToken.length > 100) { + throw new Error('A valid dungeon run token is required.') + } + + const context = database.prepare(` + SELECT + encounters.id, + encounters.name AS encounterName, + encounters.dungeon_id AS dungeonId, + dungeons.content_type AS contentType, + difficulties.id AS difficultyId, + difficulties.name AS difficultyName, + difficulties.dropped_item_level AS droppedItemLevel, + difficulties.unlock_level AS unlockLevel + FROM encounters + JOIN dungeons ON dungeons.id = encounters.dungeon_id + JOIN dungeon_difficulties + ON dungeon_difficulties.dungeon_id = encounters.dungeon_id + JOIN difficulties + ON difficulties.id = dungeon_difficulties.difficulty_id + WHERE encounters.id = ? AND difficulties.id = ? + `).get(encounterId, difficultyId) + if (!context) throw new Error('That loot table is not available.') + + const character = database.prepare('SELECT level FROM characters WHERE id = ?').get(characterId) + if (character.level < context.unlockLevel) { + throw new Error(`${context.difficultyName} unlocks at level ${context.unlockLevel}.`) + } + + const entries = database.prepare(` + SELECT + encounter_loot.drop_weight AS dropWeight, + encounter_loot.drop_chance AS dropChance, + items.id, + items.slug, + items.name, + items.slot, + items.rarity, + items.item_level AS itemLevel, + items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, + items.glyph, + items.description, + item_sets.id AS setId, + item_sets.slug AS setSlug, + item_sets.name AS setName + FROM encounter_loot + JOIN items ON items.id = encounter_loot.item_id + LEFT JOIN item_set_items ON item_set_items.item_id = items.id + LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id + WHERE encounter_loot.encounter_id = ? + AND encounter_loot.difficulty_id = ? + ORDER BY items.id + `).all(encounterId, difficultyId) + if (entries.length === 0) throw new Error('This encounter has no configured loot.') + + const dropChance = entries[0].dropChance + const findExistingRoll = database.prepare(` + SELECT + id, + item_id AS itemId, + dropped, + was_duplicate AS wasDuplicate, + quantity_after AS quantityAfter + FROM encounter_loot_rolls + WHERE character_id = ? + AND run_token = ? + AND encounter_id = ? + AND difficulty_id = ? + `) + const existing = findExistingRoll.get(characterId, runToken, encounterId, difficultyId) + if (existing) { + return formatLootRoll(database, context, existing, dropChance) + } + + database.exec('BEGIN IMMEDIATE') + try { + const concurrentRoll = findExistingRoll.get( + characterId, + runToken, + encounterId, + difficultyId, + ) + if (concurrentRoll) { + database.exec('COMMIT') + return formatLootRoll(database, context, concurrentRoll, dropChance) + } + + const selectedQuantities = new Map() + const lootChanceSlots = context.contentType === 'raid' ? 8 : 5 + for (let index = 0; index < lootChanceSlots; index += 1) { + if (Math.random() >= dropChance) continue + const selected = rollWeightedLootEntry(entries) + selectedQuantities.set( + selected.id, + (selectedQuantities.get(selected.id) ?? 0) + componentDropQuantity(context.droppedItemLevel), + ) + } + + if (selectedQuantities.size === 0) { + database.prepare(` + INSERT INTO encounter_loot_rolls + (character_id, run_token, encounter_id, difficulty_id, dropped) + VALUES (?, ?, ?, ?, 0) + `).run(characterId, runToken, encounterId, difficultyId) + database.exec('COMMIT') + return formatLootRoll(database, context, { + id: null, + itemId: null, + dropped: 0, + wasDuplicate: 0, + quantityAfter: 0, + }, dropChance) + } + + const primaryItemId = selectedQuantities.keys().next().value + const primaryPreviousQuantity = database.prepare(` + SELECT quantity + FROM character_inventory + WHERE character_id = ? AND item_id = ? + `).get(characterId, primaryItemId)?.quantity ?? 0 + const rollResult = database.prepare(` + INSERT INTO encounter_loot_rolls + ( + character_id, + run_token, + encounter_id, + difficulty_id, + item_id, + dropped, + was_duplicate, + quantity_after + ) + VALUES (?, ?, ?, ?, ?, 1, ?, ?) + `).run( + characterId, + runToken, + encounterId, + difficultyId, + primaryItemId, + primaryPreviousQuantity > 0 ? 1 : 0, + primaryPreviousQuantity + selectedQuantities.get(primaryItemId), + ) + const rollId = Number(rollResult.lastInsertRowid) + for (const [itemId, quantity] of selectedQuantities) { + const previousQuantity = database.prepare(` + SELECT quantity + FROM character_inventory + WHERE character_id = ? AND item_id = ? + `).get(characterId, itemId)?.quantity ?? 0 + database.prepare(` + INSERT INTO character_inventory (character_id, item_id, quantity, equipped) + VALUES (?, ?, ?, 0) + ON CONFLICT(character_id, item_id) + DO UPDATE SET quantity = quantity + ? + `).run(characterId, itemId, quantity, quantity) + database.prepare(` + INSERT INTO encounter_loot_roll_items + (roll_id, item_id, quantity, was_duplicate, quantity_after) + VALUES (?, ?, ?, ?, ?) + `).run( + rollId, + itemId, + quantity, + previousQuantity > 0 ? 1 : 0, + previousQuantity + quantity, + ) + } + database.exec('COMMIT') + + return formatLootRoll(database, context, { + id: rollId, + itemId: primaryItemId, + dropped: 1, + wasDuplicate: primaryPreviousQuantity > 0 ? 1 : 0, + quantityAfter: primaryPreviousQuantity + selectedQuantities.get(primaryItemId), + }, dropChance) + } catch (error) { + database.exec('ROLLBACK') + throw error + } +} + +function equipItem(database, characterId, itemId) { + const item = database.prepare(` + SELECT + items.id, + items.slot, + character_inventory.equipped + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + WHERE character_inventory.character_id = ? + AND items.id = ? + `).get(characterId, itemId) + if (!item) throw new Error('That item is not in the character inventory.') + if (!equipmentSlots.includes(item.slot)) throw new Error('That item cannot be equipped.') + if (item.equipped) return getProfile(database, characterId) + + database.exec('BEGIN') + try { + database.prepare(` + UPDATE character_inventory + SET equipped = 0 + WHERE character_id = ? + AND item_id IN (SELECT id FROM items WHERE slot = ?) + `).run(characterId, item.slot) + database.prepare(` + UPDATE character_inventory + SET equipped = 1 + WHERE character_id = ? AND item_id = ? + `).run(characterId, itemId) + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function discardExtraItem(database, characterId, itemId) { + const item = database.prepare(` + SELECT + items.name, + character_inventory.quantity + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + WHERE character_inventory.character_id = ? + AND items.id = ? + `).get(characterId, itemId) + if (!item) throw new Error('That item is not in the character inventory.') + if (item.quantity <= 1) { + throw new Error('Only extra copies can be discarded.') + } + + database.exec('BEGIN') + try { + const result = database.prepare(` + UPDATE character_inventory + SET quantity = quantity - 1 + WHERE character_id = ? + AND item_id = ? + AND quantity > 1 + `).run(characterId, itemId) + if (result.changes !== 1) { + throw new Error('The extra copy is no longer available.') + } + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function breakdownItem(database, characterId, itemId) { + const item = database.prepare(` + SELECT + items.id, + items.name, + items.slot, + items.item_level AS itemLevel, + character_inventory.quantity AS qty, + character_inventory.equipped + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + WHERE character_inventory.character_id = ? + AND items.id = ? + `).get(characterId, itemId) + if (!item) throw new Error('That item is not in the character inventory.') + if (item.slot === componentSlot) throw new Error('Components cannot be broken down.') + if (item.equipped && item.qty <= 1) { + throw new Error('Equipped items cannot be broken down.') + } + + const component = database.prepare(` + SELECT id + FROM items + WHERE slot = ? + AND item_level <= ? + ORDER BY item_level DESC, id + LIMIT 1 + `).get(componentSlot, item.itemLevel) + if (!component) throw new Error('No component type exists for this item level.') + + const count = Math.floor(Math.random() * 3) + 1 + + database.exec('BEGIN') + try { + if (item.qty <= 1) { + database.prepare(` + DELETE FROM character_inventory + WHERE character_id = ? AND item_id = ? + `).run(characterId, itemId) + } else { + database.prepare(` + UPDATE character_inventory + SET quantity = quantity - 1 + WHERE character_id = ? AND item_id = ? AND quantity > 1 + `).run(characterId, itemId) + } + + database.prepare(` + INSERT INTO character_inventory (character_id, item_id, quantity, equipped) + VALUES (?, ?, ?, 0) + ON CONFLICT(character_id, item_id) + DO UPDATE SET quantity = quantity + ? + `).run(characterId, component.id, count, count) + + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function craftItem(database, characterId, recipeId) { + const recipe = database.prepare(` + SELECT + crafting_recipes.id, + crafting_recipes.item_id AS itemId, + crafting_recipes.difficulty_id AS difficultyId, + crafting_recipes.source_dungeon_id AS sourceDungeonId, + crafting_recipes.source_encounter_id AS sourceEncounterId + FROM crafting_recipes + WHERE crafting_recipes.id = ? + `).get(recipeId) + if (!recipe) throw new Error('That crafting recipe does not exist.') + + const components = database.prepare(` + SELECT + crafting_recipe_components.item_id AS itemId, + crafting_recipe_components.quantity, + COALESCE(character_inventory.quantity, 0) AS owned + FROM crafting_recipe_components + LEFT JOIN character_inventory + ON character_inventory.item_id = crafting_recipe_components.item_id + AND character_inventory.character_id = ? + WHERE crafting_recipe_components.recipe_id = ? + `).all(characterId, recipeId) + if (components.length === 0) throw new Error('That recipe has no component requirements.') + const missing = components.find((component) => component.owned < component.quantity) + if (missing) { + const item = itemById(database, missing.itemId) + throw new Error(`Need ${missing.quantity} ${item?.name ?? 'component'} to craft this item.`) + } + + database.exec('BEGIN') + try { + for (const component of components) { + database.prepare(` + UPDATE character_inventory + SET quantity = quantity - ? + WHERE character_id = ? AND item_id = ? + `).run(component.quantity, characterId, component.itemId) + database.prepare(` + DELETE FROM character_inventory + WHERE character_id = ? AND item_id = ? AND quantity <= 0 + `).run(characterId, component.itemId) + } + + database.prepare(` + INSERT INTO character_inventory (character_id, item_id, quantity, equipped) + VALUES (?, ?, 1, 0) + ON CONFLICT(character_id, item_id) + DO UPDATE SET quantity = quantity + 1 + `).run(characterId, recipe.itemId) + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function allocateTalent(database, characterId, talentId) { + const character = database.prepare(` + SELECT class_id AS classId, talent_points AS talentPoints + FROM characters + WHERE id = ? + `).get(characterId) + const talent = database.prepare(` + SELECT + id, + class_id AS classId, + name, + max_rank AS maxRank, + tier, + prerequisite_talent_id AS prerequisiteTalentId, + prerequisite_rank AS prerequisiteRank + FROM talents + WHERE id = ? + `).get(talentId) + + if (!talent || talent.classId !== character.classId) { + throw new Error('That talent does not belong to the active class.') + } + if (character.talentPoints <= 0) { + throw new Error('No talent points are available.') + } + + const currentRank = database.prepare(` + SELECT rank + FROM character_talents + WHERE character_id = ? AND talent_id = ? + `).get(characterId, talentId)?.rank ?? 0 + if (currentRank >= talent.maxRank) { + throw new Error('That talent is already at maximum rank.') + } + + const lowerTierPoints = database.prepare(` + SELECT COALESCE(SUM(character_talents.rank), 0) AS points + FROM character_talents + JOIN talents ON talents.id = character_talents.talent_id + WHERE character_talents.character_id = ? + AND talents.class_id = ? + AND talents.tier < ? + `).get(characterId, character.classId, talent.tier).points + const requiredTierPoints = (talent.tier - 1) * 5 + if (lowerTierPoints < requiredTierPoints) { + throw new Error(`Spend ${requiredTierPoints} points in earlier tiers first.`) + } + + if (talent.prerequisiteTalentId) { + const prerequisiteRank = database.prepare(` + SELECT rank + FROM character_talents + WHERE character_id = ? AND talent_id = ? + `).get(characterId, talent.prerequisiteTalentId)?.rank ?? 0 + if (prerequisiteRank < talent.prerequisiteRank) { + throw new Error(`The prerequisite talent requires rank ${talent.prerequisiteRank}.`) + } + } + + database.exec('BEGIN') + try { + database.prepare(` + INSERT INTO character_talents (character_id, talent_id, rank) + VALUES (?, ?, 1) + ON CONFLICT(character_id, talent_id) + DO UPDATE SET rank = rank + 1 + `).run(characterId, talentId) + database.prepare(` + UPDATE characters SET talent_points = talent_points - 1 WHERE id = ? + `).run(characterId) + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function resetTalents(database, characterId) { + const character = database.prepare(` + SELECT class_id AS classId, level, talent_points AS talentPoints + FROM characters + WHERE id = ? + `).get(characterId) + const refunded = database.prepare(` + SELECT COALESCE(SUM(character_talents.rank), 0) AS points + FROM character_talents + JOIN talents ON talents.id = character_talents.talent_id + WHERE character_talents.character_id = ? + AND talents.class_id = ? + `).get(characterId, character.classId).points + + database.exec('BEGIN') + try { + database.prepare(` + DELETE FROM character_talents + WHERE character_id = ? + AND talent_id IN (SELECT id FROM talents WHERE class_id = ?) + `).run(characterId, character.classId) + database.prepare(` + UPDATE characters + SET talent_points = MIN(level, talent_points + ?) + WHERE id = ? + `).run(refunded, characterId) + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + return getProfile(database, characterId) +} + +function completeDungeon(database, characterId, accountId, dungeonId, difficultyId, runMetrics) { + const resourceSpent = Number(runMetrics?.resourceSpent) + const durationSeconds = Number(runMetrics?.durationSeconds) + if (!Number.isInteger(resourceSpent) || resourceSpent < 0 || resourceSpent > 100000) { + throw new Error('The run resource total is invalid.') + } + if (!Number.isInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 86400) { + throw new Error('The run duration is invalid.') + } + + const dungeon = database.prepare(` + SELECT + dungeons.id, + dungeons.name, + dungeons.content_type AS contentType, + dungeons.completion_item_level AS completionItemLevel, + dungeons.experience_reward AS experienceReward, + difficulties.id AS difficultyId, + difficulties.name AS difficultyName, + difficulties.unlock_level AS unlockLevel, + difficulties.experience_multiplier AS experienceMultiplier, + difficulties.dropped_item_level AS droppedItemLevel + FROM dungeons + JOIN dungeon_difficulties + ON dungeon_difficulties.dungeon_id = dungeons.id + JOIN difficulties + ON difficulties.id = dungeon_difficulties.difficulty_id + WHERE dungeons.id = ? AND difficulties.id = ? + `).get(dungeonId, difficultyId) + if (!dungeon) throw new Error('That difficulty is not available for this dungeon.') + + const character = database.prepare(` + SELECT + characters.id, + characters.class_id AS classId, + characters.name, + characters.level, + characters.experience, + characters.talent_points AS talentPoints, + classes.name AS className + FROM characters + JOIN classes ON classes.id = characters.class_id + WHERE characters.id = ? + `).get(characterId) + if (character.level < dungeon.unlockLevel) { + throw new Error(`${dungeon.difficultyName} unlocks at level ${dungeon.unlockLevel}.`) + } + const maxLevel = Number( + database.prepare("SELECT value FROM game_settings WHERE key = 'max_level'").get()?.value ?? 25, + ) + const maxTalentPoints = Number( + database.prepare("SELECT value FROM game_settings WHERE key = 'max_talent_points'").get()?.value ?? 25, + ) + const maxExperience = database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(maxLevel).experienceRequired + const completedPart = Math.min(Math.max(Number(runMetrics?.completedPart) || 1, 1), 3) + const startPart = Math.min(Math.max(Number(runMetrics?.startPart) || 1, 1), 3) + const completedParts = completedPart - startPart + 1 + const rawPartDurations = runMetrics?.partDurationSeconds + const partDurationSeconds = Array.isArray(rawPartDurations) && rawPartDurations.length === 3 + ? rawPartDurations.map(Number) + : null + const experienceReward = Math.round( + dungeon.experienceReward * dungeon.experienceMultiplier * completedPart, + ) + const newExperience = Math.min(character.experience + experienceReward, maxExperience) + const newLevel = database.prepare(` + SELECT MAX(level) AS level + FROM level_progression + WHERE experience_required <= ? + `).get(newExperience).level + const levelsGained = Math.max(0, newLevel - character.level) + const newTalentPoints = Math.min( + maxTalentPoints, + character.talentPoints + levelsGained, + ) + const unlockedAbilities = database.prepare(` + SELECT id, name, unlock_level AS unlockLevel, glyph + FROM spells + WHERE class_id = ? + AND unlock_level > ? + AND unlock_level <= ? + ORDER BY unlock_level, id + `).all(character.classId, character.level, newLevel) + const averageItemLevel = database.prepare(` + SELECT COALESCE(SUM(items.item_level), 0) * 1.0 / ? AS value + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + WHERE character_inventory.character_id = ? + AND character_inventory.equipped = 1 + `).get(equipmentSlots.length, characterId).value + + const isFullRun = startPart === 1 && completedPart >= 3 + const runRows = isFullRun + ? [ + { startPart: 1, completedParts: 1, duration: Math.max(1, Math.round(partDurationSeconds[0] || 1)) }, + { startPart: 2, completedParts: 1, duration: Math.max(1, Math.round(partDurationSeconds[1] || 1)) }, + { startPart: 3, completedParts: 1, duration: Math.max(1, Math.round(partDurationSeconds[2] || 1)) }, + { startPart: 1, completedParts: 3, duration: durationSeconds }, + ] + : [{ startPart, completedParts: completedParts, duration: durationSeconds }] + + database.exec('BEGIN') + try { + database.prepare(` + UPDATE characters + SET experience = ?, level = ?, talent_points = ? + WHERE id = ? + `).run(newExperience, newLevel, newTalentPoints, characterId) + if (dungeon.contentType === 'raid') { + database.prepare(` + UPDATE accounts + SET completed_raid_phases = MAX(completed_raid_phases, ?) + WHERE id = ? + `).run(completedPart, accountId) + } else { + database.prepare(` + UPDATE accounts + SET completed_dungeon_parts = MAX(completed_dungeon_parts, ?) + WHERE id = ? + `).run(completedPart, accountId) + } + const insertStmt = database.prepare(` + INSERT INTO dungeon_runs + ( + character_id, dungeon_id, difficulty_id, result, + character_name, class_name, character_level, + average_item_level, resource_spent, duration_seconds, + leaderboard_eligible, start_part, completed_parts, completed_at + ) + VALUES (?, ?, ?, 'victory', ?, ?, ?, ?, ?, ?, 1, ?, ?, CURRENT_TIMESTAMP) + `) + for (const row of runRows) { + insertStmt.run( + characterId, dungeonId, difficultyId, + character.name, character.className, character.level, + averageItemLevel, resourceSpent, row.duration, + row.startPart, row.completedParts, + ) + } + database.exec('COMMIT') + } catch (error) { + database.exec('ROLLBACK') + throw error + } + + let bonusItem = null + if (startPart === 1 && completedPart >= 3) { + const bonusItems = database.prepare(` + SELECT items.id, items.slug, items.name, items.slot, items.rarity, + items.item_level AS itemLevel, items.healing_power AS healingPower, + items.max_resource_bonus AS maxResourceBonus, items.glyph, items.description + FROM dungeon_completion_loot + JOIN items ON items.id = dungeon_completion_loot.item_id + WHERE dungeon_completion_loot.dungeon_id = ? + AND items.item_level >= ? + ORDER BY items.item_level, RANDOM() + LIMIT 1 + `).all(dungeonId, dungeon.completionItemLevel ?? dungeon.droppedItemLevel + 3) + if (bonusItems.length > 0) { + bonusItem = bonusItems[0] + const previousQuantity = database.prepare(` + SELECT quantity FROM character_inventory + WHERE character_id = ? AND item_id = ? + `).get(characterId, bonusItem.id)?.quantity ?? 0 + database.prepare(` + INSERT INTO character_inventory (character_id, item_id, quantity, equipped) + VALUES (?, ?, 1, 0) + ON CONFLICT(character_id, item_id) + DO UPDATE SET quantity = quantity + 1 + `).run(characterId, bonusItem.id) + bonusItem = { ...bonusItem, duplicate: previousQuantity > 0, quantityAfter: previousQuantity + 1 } + } + } + + return { + dungeonName: dungeon.name, + difficultyName: dungeon.difficultyName, + droppedItemLevel: dungeon.droppedItemLevel, + experienceGained: newExperience - character.experience, + previousLevel: character.level, + newLevel, + levelsGained, + talentPointsGained: levelsGained, + resourceSpent, + durationSeconds, + averageItemLevel, + unlockedAbilities, + bonusItem, + profile: getProfile(database, characterId, accountId), + } +} + +function completeRoguelike(database, characterId, accountId, runMetrics) { + const dungeonId = Number(runMetrics?.dungeonId) + const difficultyId = Number(runMetrics?.difficultyId) + const encountersCleared = Number(runMetrics?.encountersCleared) + const bossesCleared = Number(runMetrics?.bossesCleared ?? Math.floor(encountersCleared / 3)) + const experienceMode = runMetrics?.experienceMode === 'pvp-boss-quarter-level' + ? 'pvp-boss-quarter-level' + : 'default' + const resourceSpent = Number(runMetrics?.resourceSpent) + const durationSeconds = Number(runMetrics?.durationSeconds) + if (!Number.isInteger(dungeonId) || dungeonId < 1) { + throw new Error('The roguelike dungeon is invalid.') + } + if (!Number.isInteger(difficultyId) || difficultyId < 1) { + throw new Error('The roguelike difficulty is invalid.') + } + if (!Number.isInteger(encountersCleared) || encountersCleared < 0 || encountersCleared > 100000) { + throw new Error('The roguelike progress total is invalid.') + } + if (!Number.isInteger(bossesCleared) || bossesCleared < 0 || bossesCleared > 100000) { + throw new Error('The roguelike boss total is invalid.') + } + if (!Number.isInteger(resourceSpent) || resourceSpent < 0 || resourceSpent > 100000) { + throw new Error('The run resource total is invalid.') + } + if (!Number.isInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 86400) { + throw new Error('The run duration is invalid.') + } + + const dungeon = database.prepare(` + SELECT + dungeons.id, + dungeons.name, + difficulties.id AS difficultyId, + difficulties.name AS difficultyName, + difficulties.unlock_level AS unlockLevel, + difficulties.experience_multiplier AS experienceMultiplier, + difficulties.dropped_item_level AS droppedItemLevel, + dungeons.experience_reward AS experienceReward + FROM dungeons + JOIN dungeon_difficulties + ON dungeon_difficulties.dungeon_id = dungeons.id + JOIN difficulties + ON difficulties.id = dungeon_difficulties.difficulty_id + WHERE dungeons.id = ? AND difficulties.id = ? + `).get(dungeonId, difficultyId) + if (!dungeon) throw new Error('That difficulty is not available for this roguelike.') + + const character = database.prepare(` + SELECT + characters.id, + characters.class_id AS classId, + characters.level, + characters.experience, + characters.talent_points AS talentPoints + FROM characters + WHERE characters.id = ? + `).get(characterId) + if (character.level < dungeon.unlockLevel) { + throw new Error(`${dungeon.difficultyName} unlocks at level ${dungeon.unlockLevel}.`) + } + const maxLevel = Number( + database.prepare("SELECT value FROM game_settings WHERE key = 'max_level'").get()?.value ?? 25, + ) + const maxTalentPoints = Number( + database.prepare("SELECT value FROM game_settings WHERE key = 'max_talent_points'").get()?.value ?? 25, + ) + const maxExperience = database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(maxLevel).experienceRequired + let newExperience = character.experience + let newLevel = character.level + if (experienceMode === 'pvp-boss-quarter-level') { + for (let bossIndex = 0; bossIndex < bossesCleared && newExperience < maxExperience; bossIndex += 1) { + const currentLevelFloor = database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(newLevel).experienceRequired + const nextLevelExperience = newLevel >= maxLevel + ? maxExperience + : database.prepare(` + SELECT experience_required AS experienceRequired + FROM level_progression + WHERE level = ? + `).get(newLevel + 1).experienceRequired + const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor) + newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * 0.25)) + newLevel = database.prepare(` + SELECT MAX(level) AS level + FROM level_progression + WHERE experience_required <= ? + `).get(newExperience).level + } + } else { + const experienceReward = Math.round( + dungeon.experienceReward * dungeon.experienceMultiplier * (encountersCleared / 3), + ) + newExperience = Math.min(character.experience + experienceReward, maxExperience) + newLevel = database.prepare(` + SELECT MAX(level) AS level + FROM level_progression + WHERE experience_required <= ? + `).get(newExperience).level + } + const levelsGained = Math.max(0, newLevel - character.level) + const newTalentPoints = Math.min( + maxTalentPoints, + character.talentPoints + levelsGained, + ) + const unlockedAbilities = database.prepare(` + SELECT id, name, unlock_level AS unlockLevel, glyph + FROM spells + WHERE class_id = ? + AND unlock_level > ? + AND unlock_level <= ? + ORDER BY unlock_level, id + `).all(character.classId, character.level, newLevel) + const averageItemLevel = database.prepare(` + SELECT COALESCE(SUM(items.item_level), 0) * 1.0 / ? AS value + FROM character_inventory + JOIN items ON items.id = character_inventory.item_id + WHERE character_inventory.character_id = ? + AND character_inventory.equipped = 1 + `).get(equipmentSlots.length, characterId).value + + database.prepare(` + UPDATE characters + SET experience = ?, level = ?, talent_points = ? + WHERE id = ? + `).run(newExperience, newLevel, newTalentPoints, characterId) + + return { + dungeonName: `${dungeon.name} Roguelike`, + difficultyName: dungeon.difficultyName, + droppedItemLevel: dungeon.droppedItemLevel, + experienceGained: newExperience - character.experience, + previousLevel: character.level, + newLevel, + levelsGained, + talentPointsGained: levelsGained, + resourceSpent, + durationSeconds, + averageItemLevel, + unlockedAbilities, + bonusItem: null, + profile: getProfile(database, characterId, accountId), + } +} + +function saveProfile(database, characterId, accountId, payload) { + const classId = Number(payload.classId) + const spellIds = Array.isArray(payload.abilitySlots) + ? payload.abilitySlots.slice(0, 6).map((value) => value === null ? null : Number(value)) + : [] + + while (spellIds.length < 6) spellIds.push(null) + + const gameClass = database.prepare('SELECT id FROM classes WHERE id = ?').get(classId) + if (!gameClass) throw new Error('Selected class does not exist.') + + const getCharacterLevel = database.prepare(` + SELECT level, class_id AS classId + FROM characters + WHERE id = ? + `) + const character = getCharacterLevel.get(characterId) + if (!character) throw new Error('Character not found.') + + const selectedIds = spellIds.filter((id) => id !== null) + if (new Set(selectedIds).size !== selectedIds.length) { + throw new Error('The same ability cannot be equipped twice.') + } + + if (selectedIds.length > 0) { + const placeholders = selectedIds.map(() => '?').join(', ') + const validSpells = database.prepare(` + SELECT id + FROM spells + WHERE class_id = ? + AND unlock_level <= ? + AND id IN (${placeholders}) + `).all(classId, character.level, ...selectedIds) + + if (validSpells.length !== selectedIds.length) { + throw new Error('One or more abilities are locked or belong to another class.') + } + } + + database.exec('BEGIN') + try { + let targetCharacterId = characterId + if (character.classId !== classId) { + const existing = database.prepare(` + SELECT id FROM characters WHERE account_id = ? AND class_id = ? + `).get(accountId, classId) + if (existing) { + targetCharacterId = existing.id + } else { + const char = database.prepare('SELECT name FROM characters WHERE id = ?').get(characterId) + targetCharacterId = initializeCharacter(database, accountId, char.name, classId) + } + database.prepare(` + UPDATE sessions SET active_character_id = ? WHERE account_id = ? + `).run(targetCharacterId, accountId) + } + database.prepare('DELETE FROM character_ability_slots WHERE character_id = ?').run(targetCharacterId) + const insertSlot = database.prepare(` + INSERT INTO character_ability_slots (character_id, slot_number, spell_id) + VALUES (?, ?, ?) + `) + spellIds.forEach((spellId, index) => { + insertSlot.run(targetCharacterId, index + 1, spellId) + }) + database.exec('COMMIT') + return targetCharacterId + } catch (error) { + database.exec('ROLLBACK') + throw error + } +} + +export function gameApiPlugin() { + return { + name: 'ashen-halls-game-api', + configureServer(server) { + server.middlewares.use(handleApiRequest) + }, + configurePreviewServer(server) { + server.middlewares.use(handleApiRequest) + }, + } +} + +export async function handleApiRequest(request, response, next) { + if (!request.url?.startsWith('/api/')) { + next() + return + } + + if (request.url.startsWith('/api/boss-images/') && request.method === 'GET') { + sendBossImage(request, response) + return + } + + if (request.url.startsWith('/api/item-images/') && request.method === 'GET') { + sendItemImage(request, response) + return + } + + if (!existsSync(databasePath)) { + sendJson(response, 503, { error: 'Database missing. Run npm run db:init.' }) + return + } + + const database = new DatabaseSync(databasePath) + database.exec('PRAGMA foreign_keys = ON') + + try { + const ip = requestIp(request) + consumeRateLimit(`api:${ip}`, 240, 60 * 1000) + database.prepare(` + DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP + `).run() + + if (request.url === '/api/auth/register' && request.method === 'POST') { + const payload = await readJson(request) + const result = registerAccount(database, request, payload) + sendJson( + response, + 201, + { account: result.account, profile: result.profile }, + { 'Set-Cookie': sessionCookie(result.token, request) }, + ) + return + } + + if (request.url === '/api/auth/login' && request.method === 'POST') { + const payload = await readJson(request) + const result = loginAccount(database, request, payload) + sendJson( + response, + 200, + { account: result.account, profile: result.profile }, + { 'Set-Cookie': sessionCookie(result.token, request) }, + ) + return + } + + if (request.url === '/api/auth/session' && request.method === 'GET') { + const session = currentSession(database, request) + if (!session) { + sendJson(response, 200, { account: null, profile: null }) + return + } + sendJson(response, 200, { + account: { id: session.accountId, username: session.username }, + profile: getProfile(database, session.characterId, session.accountId), + }) + return + } + + if (request.url === '/api/auth/logout' && request.method === 'POST') { + const token = parseCookies(request)[sessionCookieName] + if (token) { + database.prepare('DELETE FROM sessions WHERE token_hash = ?').run(tokenHash(token)) + } + sendJson( + response, + 200, + { ok: true }, + { 'Set-Cookie': sessionCookie('', request, 0) }, + ) + return + } + + const session = requireSession(database, request) + + if (request.url === '/api/profile' && request.method === 'GET') { + sendJson(response, 200, getProfile(database, session.characterId, session.accountId)) + return + } + + if (request.url === '/api/profile' && request.method === 'PUT') { + const payload = await readJson(request) + const newCharacterId = saveProfile(database, session.characterId, session.accountId, payload) + sendJson(response, 200, getProfile(database, newCharacterId, session.accountId)) + return + } + + const dungeonCompletion = request.url.match(/^\/api\/dungeons\/(\d+)\/complete$/) + if (dungeonCompletion && request.method === 'POST') { + const payload = await readJson(request) + sendJson( + response, + 200, + completeDungeon( + database, + session.characterId, + session.accountId, + Number(dungeonCompletion[1]), + Number(payload.difficultyId), + payload, + ), + ) + return + } + + if (request.url === '/api/roguelike/complete' && request.method === 'POST') { + const payload = await readJson(request) + sendJson( + response, + 200, + completeRoguelike(database, session.characterId, session.accountId, payload), + ) + return + } + + const talentAllocation = request.url.match(/^\/api\/talents\/(\d+)\/allocate$/) + if (talentAllocation && request.method === 'POST') { + sendJson( + response, + 200, + allocateTalent(database, session.characterId, Number(talentAllocation[1])), + ) + return + } + + if (request.url === '/api/talents/reset' && request.method === 'POST') { + sendJson(response, 200, resetTalents(database, session.characterId)) + return + } + + const itemEquip = request.url.match(/^\/api\/equipment\/(\d+)\/equip$/) + if (itemEquip && request.method === 'POST') { + sendJson( + response, + 200, + equipItem(database, session.characterId, Number(itemEquip[1])), + ) + return + } + + const itemDiscard = request.url.match(/^\/api\/equipment\/(\d+)\/discard-extra$/) + if (itemDiscard && request.method === 'POST') { + sendJson( + response, + 200, + discardExtraItem(database, session.characterId, Number(itemDiscard[1])), + ) + return + } + + const itemBreakdown = request.url.match(/^\/api\/equipment\/(\d+)\/breakdown$/) + if (itemBreakdown && request.method === 'POST') { + sendJson( + response, + 200, + breakdownItem(database, session.characterId, Number(itemBreakdown[1])), + ) + return + } + + const recipeCraft = request.url.match(/^\/api\/crafting\/recipes\/(\d+)\/craft$/) + if (recipeCraft && request.method === 'POST') { + sendJson( + response, + 200, + craftItem(database, session.characterId, Number(recipeCraft[1])), + ) + return + } + + const encounterLootRoll = request.url.match(/^\/api\/encounters\/(\d+)\/loot-roll$/) + if (encounterLootRoll && request.method === 'POST') { + const payload = await readJson(request) + sendJson( + response, + 200, + rollEncounterLoot( + database, + session.characterId, + Number(encounterLootRoll[1]), + Number(payload.difficultyId), + payload.runToken, + ), + ) + return + } + + sendJson(response, 404, { error: 'API route not found.' }) + } catch (error) { + const status = Number(error?.status) || 400 + const headers = error?.retryAfter + ? { 'Retry-After': String(error.retryAfter) } + : {} + sendJson( + response, + status, + { error: error instanceof Error ? error.message : 'Unable to process request.' }, + headers, + ) + } finally { + database.close() + } +} diff --git a/server/production.mjs b/server/production.mjs new file mode 100644 index 0000000..655613f --- /dev/null +++ b/server/production.mjs @@ -0,0 +1,77 @@ +import { createReadStream, existsSync, statSync } from 'node:fs' +import { createServer } from 'node:http' +import { extname, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { handleApiRequest } from './game-api.mjs' + +process.env.NODE_ENV = 'production' + +const distPath = fileURLToPath(new URL('../dist', import.meta.url)) +const indexPath = resolve(distPath, 'index.html') +const host = process.env.HOST ?? '127.0.0.1' +const port = Number(process.env.PORT ?? 4173) +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', +} + +function sendFile(response, filePath) { + response.statusCode = 200 + response.setHeader( + 'Content-Type', + contentTypes[extname(filePath).toLowerCase()] ?? 'application/octet-stream', + ) + response.setHeader('X-Content-Type-Options', 'nosniff') + response.setHeader('Referrer-Policy', 'same-origin') + response.setHeader('X-Frame-Options', 'DENY') + createReadStream(filePath).pipe(response) +} + +async function serveStatic(request, response) { + const requestPath = decodeURIComponent(new URL(request.url, 'http://localhost').pathname) + const candidate = resolve(distPath, `.${requestPath}`) + const insideDist = candidate === distPath || candidate.startsWith(`${distPath}${sep}`) + + if (requestPath === '/admin.html') { + response.statusCode = 404 + response.end('Not found') + return + } + + if ( + insideDist + && existsSync(candidate) + && statSync(candidate).isFile() + ) { + sendFile(response, candidate) + return + } + + if (!existsSync(indexPath)) { + response.statusCode = 503 + response.end('Build missing. Run npm run build.') + return + } + sendFile(response, indexPath) +} + +const server = createServer((request, response) => { + handleApiRequest(request, response, () => { + serveStatic(request, response).catch(() => { + response.statusCode = 500 + response.end('Unable to serve the application.') + }) + }) +}) + +server.listen(port, host, () => { + console.log(`I want to Heal listening on http://${host}:${port}`) +}) diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..bb1ca7b --- /dev/null +++ b/src/App.css @@ -0,0 +1,4955 @@ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap'); + +:root { + --ink: #f4eed8; + --muted: #a89f87; + --panel: #191b22; + --panel-light: #242630; + --edge: #565066; + --gold: #e5b95f; + --red: #a73543; + --red-bright: #dc5162; + --green: #3f9a66; + --blue: #3477bb; + --purple: #8e68c4; +} + +* { + box-sizing: border-box; +} + +button { + font: inherit; +} + +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible, +[tabindex]:focus-visible { + outline: 3px solid #fff4a8 !important; + box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65); +} + +.settings-heading { + align-items: end; + border-bottom: 2px solid #34343d; + display: flex; + justify-content: space-between; + padding: 12px 0 22px; +} + +.settings-heading > p { + color: var(--muted); + font-size: 19px; +} + +.binding-tabs { + display: grid; + gap: 10px; + grid-template-columns: 1fr 1fr; + margin: 22px 0; +} + +.binding-tabs button, +.binding-list button { + background: #15171d; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + outline: 2px solid #41404a; +} + +.binding-tabs button { + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 48px; +} + +.binding-tabs button.selected { + color: var(--gold); + outline-color: var(--gold); +} + +.binding-list { + display: grid; + gap: 9px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.binding-list button { + align-items: center; + display: flex; + justify-content: space-between; + min-height: 54px; + padding: 10px 13px; + text-align: left; +} + +.binding-list button:hover, +.binding-list button.listening { + outline-color: var(--gold); +} + +.binding-list button > span { + color: var(--muted); + font-size: 18px; +} + +.binding-list kbd { + align-items: center; + color: var(--gold); + display: inline-flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + justify-content: flex-end; + min-height: 25px; + text-align: right; +} + +.controller-face-icon { + --button-color: var(--gold); + align-items: center; + background: var(--button-color); + border: 2px solid #07080a; + border-radius: 999px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.28), 0 2px 0 #07080a; + color: #fff8e6; + display: inline-flex; + flex: 0 0 auto; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; + font-weight: 900; + height: 24px; + justify-content: center; + line-height: 1; + min-width: 24px; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.65); + vertical-align: middle; +} + +.controller-face-playstation { + background: transparent; + border: 0; + box-shadow: none; + color: var(--button-color); + font-size: 25px; + height: 26px; + min-width: 26px; + text-shadow: 0 1px 0 #07080a, 0 0 8px rgba(0, 0, 0, 0.5); +} + +.controller-face-nintendo { + border-color: #fff8e6; +} + +.settings-footer { + align-items: center; + border-top: 2px solid #34343d; + color: var(--muted); + display: flex; + font-size: 18px; + justify-content: space-between; + margin-top: 22px; + padding-top: 18px; +} + +.controller-preferences { + align-items: center; + background: #111319; + border: 2px solid #090a0d; + display: grid; + gap: 14px; + grid-template-columns: minmax(0, 1fr) auto; + margin-top: 22px; + outline: 2px solid #41404a; + padding: 18px; +} + +.controller-preferences p:not(.eyebrow) { + color: var(--muted); + font-size: 17px; + margin-top: 6px; +} + +.controller-preferences button, +.controller-icon-options { + background: #15171d; + border: 2px solid #090a0d; + color: var(--ink); + outline: 2px solid #41404a; +} + +.controller-preferences button { + cursor: pointer; + min-height: 42px; + padding: 8px 12px; +} + +.controller-preferences button.selected { + color: var(--gold); + outline-color: var(--gold); +} + +.controller-icon-options { + align-items: center; + display: grid; + gap: 8px; + grid-column: 1 / -1; + grid-template-columns: minmax(140px, 1fr) repeat(3, minmax(135px, auto)); + padding: 8px; +} + +.controller-icon-options > span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + padding-left: 7px; +} + +.controller-icon-options button { + align-items: center; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 8px; + justify-content: center; +} + +.controller-style-preview { + align-items: center; + display: inline-grid; + gap: 3px; + grid-auto-flow: column; +} + +.controller-style-preview .controller-face-icon { + height: 21px; + min-width: 21px; +} + +.controller-style-preview .controller-face-playstation { + font-size: 22px; +} + +.controller-style-name { + color: var(--ink); + line-height: 1.3; +} + +.dual-screen-settings { + background: #111319; + border: 2px solid #090a0d; + display: grid; + gap: 18px; + grid-template-columns: minmax(0, 1fr) auto; + margin: 12px 0 24px; + outline: 2px solid #41404a; + padding: 20px; +} + +.dual-screen-settings p:not(.eyebrow) { + color: var(--muted); + font-size: 19px; + line-height: 1.2; + margin-top: 8px; + max-width: 680px; +} + +.dual-screen-settings > small { + color: var(--muted); + font-size: 16px; + grid-column: 1 / -1; +} + +.android-display-list { + display: grid; + gap: 7px; + grid-column: 1 / -1; +} + +.android-display-list span { + background: #090b10; + color: var(--muted); + display: flex; + font-size: 16px; + gap: 10px; + padding: 8px 10px; +} + +.android-display-list strong { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.dual-screen-actions { + display: grid; + gap: 9px; + min-width: 260px; +} + +.dual-screen-actions button, +.dual-bottom-status button, +.dual-bottom-display button, +.dual-top-game-toolbar button { + background: #242630; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + min-height: 44px; + outline: 2px solid #4b4855; + padding: 8px 14px; +} + +.dual-screen-actions button.selected, +.dual-bottom-status.connected { + outline-color: var(--green); +} + +.binding-capture, +.controller-keyboard-backdrop { + align-items: center; + background: rgba(5, 6, 9, 0.88); + display: flex; + inset: 0; + justify-content: center; + position: fixed; + z-index: 100; +} + +.binding-capture > div { + background: var(--panel); + border: 3px solid #090a0d; + box-shadow: 8px 8px 0 #050609; + max-width: 480px; + outline: 2px solid var(--gold); + padding: 30px; + text-align: center; + width: calc(100% - 32px); +} + +.binding-capture p:not(.eyebrow) { + color: var(--muted); + font-size: 21px; + margin: 18px 0; +} + +.binding-capture button, +.controller-keyboard button { + background: #242630; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + outline: 2px solid #4b4855; +} + +.binding-capture button { + min-height: 44px; + padding: 8px 24px; +} + +.controller-keyboard { + background: var(--panel); + border: 3px solid #090a0d; + box-shadow: 8px 8px 0 #050609; + max-width: 820px; + outline: 2px solid var(--gold); + padding: 20px; + width: calc(100% - 28px); +} + +.controller-keyboard-heading { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 14px; +} + +.controller-keyboard-heading strong { + display: block; + font-size: 21px; + max-width: 650px; + min-height: 25px; + overflow: hidden; +} + +.controller-keyboard-heading button { + min-height: 42px; + padding: 8px 18px; +} + +.controller-keyboard-grid { + display: grid; + gap: 7px; + grid-template-columns: repeat(10, 1fr); +} + +.controller-keyboard-grid button { + font-family: 'Press Start 2P', monospace; + font-size: 9px; + min-height: 43px; +} + +.controller-keyboard-actions { + display: grid; + gap: 8px; + grid-template-columns: 1fr 2fr 1.5fr 1fr; + margin-top: 10px; +} + +.controller-keyboard-actions button { + min-height: 45px; +} + +.controller-keyboard-actions button.active { + color: var(--gold); + outline-color: var(--gold); +} + +.target-controls { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 6px; + gap: 18px; + justify-content: flex-end; + margin: 8px 0 10px; +} + +.target-controls span { + align-items: center; + display: inline-flex; + gap: 5px; +} + +.dual-bottom-shell { + width: min(1220px, calc(100% - 20px)); +} + +.dual-bottom-shell > .topbar { + padding-bottom: 14px; + padding-top: 14px; +} + +.dual-bottom-status { + align-items: center; + background: var(--panel); + border: 3px solid #0c0d11; + box-shadow: 7px 7px 0 #08090c; + display: grid; + gap: 20px; + grid-template-columns: auto minmax(220px, 1fr) auto; + margin-top: 18px; + outline: 2px solid var(--red); + padding: 14px 18px; +} + +.dual-bottom-status strong { + color: var(--red-bright); + font-family: 'Press Start 2P', monospace; + font-size: 9px; +} + +.dual-bottom-status.connected strong { + color: #76d39a; +} + +.dual-bottom-fallback { + align-items: center; + display: grid; + gap: 10px; + grid-template-columns: auto minmax(150px, 1fr) auto; +} + +.dual-bottom-fallback > span, +.dual-bottom-fallback > small { + color: var(--muted); +} + +.dual-bottom-actions { + margin-top: 18px; + min-height: 440px; + padding: 28px; +} + +.dual-bottom-actions .active-target-card { + min-width: 390px; +} + +.dual-bottom-actions .mana-wrap { + width: min(520px, 48%); +} + +.dual-bottom-actions .spell { + min-height: 190px; +} + +.dual-bottom-actions .spell-icon { + height: 66px; + margin-top: 25px; + width: 66px; +} + +.dual-bottom-actions .spell strong { + font-size: 19px; +} + +.dual-bottom-actions .spell small { + font-size: 17px; +} + +.dual-top-shell { + background: + linear-gradient(rgba(8, 9, 12, 0.91), rgba(8, 9, 12, 0.91)), + repeating-linear-gradient(0deg, #171922 0 2px, #11131a 2px 4px); + color: var(--ink); + display: grid; + gap: 16px; + grid-template-rows: auto auto 1fr auto; + min-height: 100vh; + padding: 18px; +} + +.dual-top-header, +.dual-top-enemy, +.dual-top-party, +.dual-top-log, +.dual-top-waiting > section { + background: var(--panel); + border: 3px solid #0c0d11; + box-shadow: 7px 7px 0 #08090c; + outline: 2px solid var(--edge); +} + +.dual-top-header { + align-items: center; + display: flex; + justify-content: space-between; + padding: 14px 20px; +} + +.dual-top-header h1 { + font-size: clamp(17px, 2.1vw, 28px); +} + +.dual-top-progress { + align-items: center; + display: flex; + gap: 18px; +} + +.dual-top-progress > span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 8px; +} + +.dual-top-enemy { + align-items: center; + display: flex; + gap: 14px; + padding: 10px 14px; +} + +.dual-top-enemy .enemy-portrait { + flex-basis: 84px; + height: 84px; +} + +.dual-top-enemy .bar { + height: 25px; +} + +.dual-top-party { + min-height: 0; + padding: 10px 14px; +} + +.dual-top-party-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.dual-top-party-grid.raid { + grid-template-rows: repeat(2, minmax(0, 1fr)); +} + +.dual-top-member { + background: var(--panel-light); + border: 2px solid #0a0b0e; + min-height: 120px; + outline: 2px solid #3a3944; + padding: 14px; +} + +.dual-top-member.selected { + background: #29291f; + outline: 3px solid var(--gold); +} + +.dual-top-member.dead { + filter: grayscale(1); + opacity: 0.48; +} + +.dual-top-member .member-header strong { + font-size: 20px; +} + +.dual-top-member .member-header small { + font-size: 17px; +} + +.dual-top-member .bar { + height: 22px; + margin-top: 14px; +} + +.dual-top-member .member-target-key { + display: inline-flex; + align-items: center; + justify-content: center; + background: #0a0b0e; + border: 2px solid #3a3944; + border-radius: 6px; + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 14px; + font-weight: 600; + margin-top: 10px; + padding: 4px 10px; + text-transform: uppercase; + white-space: nowrap; + gap: 4px; +} + +.dual-top-member .member-target-key kbd { + font-family: 'Press Start 2P', monospace; + font-size: inherit; +} + +.dual-top-member .member-target-key svg { + display: inline-block; + vertical-align: middle; +} + +.dual-top-log { + display: flex; + gap: 14px; + min-height: 36px; + overflow: hidden; + padding: 6px 14px; +} + +.dual-top-log span { + color: var(--muted); + flex: 1; + font-size: 17px; +} + +.dual-top-log .heal { + color: #78d79d; +} + +.dual-top-log .danger { + color: #ff8190; +} + +.dual-top-log .loot { + color: var(--gold); +} + +.dual-top-waiting { + align-items: center; + display: flex; + justify-content: center; +} + +.dual-top-waiting > section { + padding: 40px; + text-align: center; +} + +.dual-top-waiting p:not(.eyebrow) { + color: var(--muted); + font-size: 22px; + margin: 15px 0; +} + +.game-shell.dual-top-game-shell { + height: 100dvh; + margin: 0 auto; + padding: 10px; + width: 100%; +} + +.dual-top-game-toolbar { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 12px; + justify-content: flex-end; + min-height: 42px; +} + +.dual-top-game-toolbar > span { + margin-right: auto; +} + +.dual-top-game-toolbar button { + min-height: 34px; +} + +.dual-top-main { + display: grid; + gap: 10px; + height: calc(100dvh - 20px); + grid-template-rows: auto 1fr auto; + min-height: 0; +} + +.dual-top-main .dual-top-enemy, +.dual-top-main .dual-top-party, +.dual-top-main .dual-top-log { + margin: 0; + width: 100%; +} + +.dual-top-main .dual-top-party-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.dual-top-main .dual-top-party-grid.raid { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.dual-top-main .dual-top-member { + color: var(--ink); + cursor: pointer; + text-align: left; + width: 100%; +} + +.dual-top-main .dual-top-member:hover { + outline-color: var(--gold); +} + +.dual-bottom-display { + background: + linear-gradient(rgba(8, 9, 12, 0.91), rgba(8, 9, 12, 0.91)), + repeating-linear-gradient(0deg, #171922 0 2px, #11131a 2px 4px); + color: var(--ink); + display: grid; + gap: 10px; + grid-template-rows: auto auto auto 1fr; + min-height: 100vh; + padding: 10px; +} + +.dual-controls-header, +.dual-controls-resource, +.dual-controls-targets, +.dual-controls-spells, +.dual-bottom-waiting > section { + background: var(--panel); + border: 3px solid #0c0d11; + outline: 2px solid var(--edge); +} + +.dual-controls-header { + align-items: center; + display: flex; + justify-content: space-between; + min-height: 70px; + padding: 10px 16px; +} + +.dual-controls-header h1 { + font-size: clamp(14px, 2.2vw, 23px); +} + +.dual-controls-progress { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.dual-controls-resource { + align-items: center; + display: grid; + gap: 20px; + grid-template-columns: minmax(180px, 0.7fr) minmax(260px, 1.3fr); + padding: 12px 16px; +} + +.dual-controls-resource strong { + font-size: 20px; +} + +.dual-controls-mana > span { + color: var(--muted); + display: block; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + margin-bottom: 6px; + text-align: right; +} + +.dual-controls-mana .bar { + height: 23px; +} + +.dual-controls-targets { + display: grid; + gap: 8px; + grid-template-columns: 1fr 1fr; + padding: 8px; +} + +.dual-controls-targets button { + align-items: center; + display: inline-flex; + gap: 6px; + justify-content: center; + min-height: 42px; +} + +.dual-controls-targets.direct { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.dual-controls-targets.direct button { + font-size: 15px; + min-height: 34px; +} + +.dual-controls-spells { + display: grid; + gap: 9px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + min-height: 0; + padding: 10px; +} + +.dual-controls-spells .spell { + min-height: 0; +} + +.dual-controls-spells .spell-icon { + height: 58px; + margin-top: 21px; + width: 58px; +} + +.dual-controls-spells .spell strong { + font-size: 18px; +} + +.dual-controls-spells .spell small { + font-size: 16px; +} + +@media (max-width: 700px), (max-height: 640px) { + .dual-bottom-display { + gap: 6px; + min-height: 100dvh; + overflow: hidden; + padding: 6px; + } + + .dual-controls-header, + .dual-controls-resource, + .dual-controls-targets, + .dual-controls-spells { + border-width: 2px; + width: 100%; + } + + .dual-controls-header { + min-height: 42px; + padding: 5px 8px; + } + + .dual-controls-header .eyebrow { + font-size: 6px; + margin-bottom: 3px; + } + + .dual-controls-header h1 { + font-size: 13px; + line-height: 1.2; + } + + .dual-controls-progress { + font-size: 6px; + } + + .dual-controls-resource { + gap: 8px; + grid-template-columns: minmax(110px, 0.8fr) minmax(150px, 1.2fr); + padding: 6px 8px; + } + + .dual-controls-resource .eyebrow { + font-size: 6px; + margin-bottom: 3px; + } + + .dual-controls-resource strong { + font-size: 16px; + } + + .dual-controls-mana > span { + font-size: 6px; + } + + .dual-controls-mana .bar { + height: 16px; + } + + .dual-controls-targets { + gap: 4px; + padding: 4px; + } + + .dual-controls-targets.direct { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .dual-controls-targets button, + .dual-controls-targets.direct button { + font-size: 12px; + min-height: 30px; + padding: 4px 5px; + } + + .dual-controls-spells { + gap: 5px; + padding: 5px; + } + + .dual-controls-spells .spell { + min-height: 0; + padding: 5px 3px; + } + + .dual-controls-spells .spell kbd { + font-size: 8px; + padding: 1px 4px; + } + + .dual-controls-spells .spell-icon { + font-size: 13px; + height: 32px; + margin-top: 16px; + width: 32px; + } + + .dual-controls-spells .spell strong { + font-size: 13px; + line-height: 1.1; + } + + .dual-controls-spells .spell small { + font-size: 12px; + } +} + +.dual-bottom-waiting { + align-items: center; + display: flex; + justify-content: center; +} + +.dual-bottom-waiting > section { + padding: 36px; + text-align: center; +} + +.dual-bottom-waiting p:not(.eyebrow) { + color: var(--muted); + font-size: 21px; + margin-top: 14px; +} + +.game-shell { + width: min(1180px, calc(100% - 28px)); + margin: 22px auto; + position: relative; +} + +.auth-shell { + align-items: center; + display: flex; + min-height: 100vh; + padding: 28px; +} + +.auth-panel { + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1.2fr) minmax(330px, 0.8fr); + margin: 0 auto; + max-width: 980px; + width: 100%; +} + +.auth-brand, +.auth-card { + background: var(--panel); + border: 2px solid #090a0d; + min-width: 0; + outline: 2px solid #41404a; + padding: 28px; +} + +.auth-brand { + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 430px; +} + +.auth-brand h1 { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: clamp(25px, 4vw, 45px); + line-height: 1.3; + margin: 10px 0 22px; +} + +.auth-brand > p:last-child { + color: var(--muted); + font-size: 24px; + line-height: 1.35; + max-width: 580px; +} + +.auth-tabs { + display: grid; + gap: 7px; + grid-template-columns: 1fr 1fr; +} + +.auth-tabs button { + background: #17181e; + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + min-height: 48px; + outline: 2px solid #3e3d47; +} + +.auth-tabs button.selected { + color: var(--gold); + outline-color: var(--gold); +} + +.auth-card form { + display: grid; + gap: 16px; + margin-top: 24px; +} + +.auth-card label { + color: var(--muted); + display: grid; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 8px; + text-transform: uppercase; +} + +.auth-card input { + background: #111217; + border: 2px solid #090a0d; + box-sizing: border-box; + color: var(--ink); + font-family: inherit; + font-size: 20px; + outline: 2px solid #3e3d47; + padding: 11px; + width: 100%; +} + +.auth-card input:focus { + outline-color: var(--gold); +} + +.auth-message { + color: var(--muted); + font-size: 16px; + line-height: 1.25; + margin-top: 18px; + min-height: 40px; +} + +.auth-message.error { + color: #ff8190; +} + +.offline-divider { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 10px; + margin: 20px 0; + text-transform: uppercase; +} + +.offline-divider::before, +.offline-divider::after { + background: #3e3d47; + content: ''; + flex: 1; + height: 2px; +} + +.offline-entry { + background: #111217; + border: 2px solid #090a0d; + display: grid; + gap: 13px; + outline: 2px solid #3e3d47; + padding: 16px; +} + +.offline-entry h2 { + color: var(--ink); + font-size: 11px; +} + +.offline-entry p:not(.eyebrow) { + color: var(--muted); + font-size: 16px; + line-height: 1.2; + margin-top: 6px; +} + +.offline-resume-button, +.offline-new-button { + cursor: pointer; + min-height: 42px; +} + +.offline-resume-button { + background: var(--gold); + border: 2px solid #090a0d; + color: #19150e; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + outline: 2px solid #816630; +} + +.offline-new-button { + border: 2px solid #3e3d47; + width: 100%; +} + +.logout-button { + background: transparent; + border: 0; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 6px; + padding: 3px 0; +} + +.logout-button:hover { + color: var(--gold); +} + +.topbar, +.enemy-card, +.party-panel, +.combat-log, +.action-panel, +.menu-screen, +.content-screen, +.message-panel { + border: 3px solid #0c0d11; + outline: 2px solid var(--edge); + background: var(--panel); + box-shadow: 7px 7px 0 #08090c; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 18px; +} + +.app-header { + min-height: 68px; +} + +.brand-button { + background: none; + border: 0; + color: var(--ink); + cursor: pointer; + padding: 0; + text-align: left; +} + +.brand-button .eyebrow { + display: block; +} + +.brand-button strong { + font-family: 'Press Start 2P', monospace; + font-size: clamp(10px, 1.3vw, 14px); +} + +.character-summary { + align-items: center; + display: flex; + gap: 12px; + text-align: right; +} + +.character-summary span, +.character-summary small { + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.character-summary strong { + color: var(--ink); + font-size: 19px; +} + +.character-summary .mode-badge { + color: #8fc7ff; + margin-top: 2px; +} + +.character-summary .mode-offline { + color: var(--gold); +} + +.header-xp { + background: #090a0d; + height: 5px; + width: 100px; +} + +.header-xp span, +.experience-bar span { + background: var(--purple); + box-shadow: inset 0 3px #b08bdf; + display: block; + height: 100%; +} + +h1, +h2, +p { + margin: 0; +} + +h1, +h2 { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + line-height: 1.45; +} + +h1 { + font-size: clamp(18px, 3vw, 28px); +} + +h2 { + font-size: 14px; +} + +.eyebrow { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + letter-spacing: 1px; + margin-bottom: 8px; + text-transform: uppercase; +} + +.menu-screen, +.content-screen, +.message-panel { + margin-top: 18px; + min-height: 0; + padding: 28px; +} + +.message-panel { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.message-panel > p:last-child { + color: var(--muted); + font-size: 22px; + margin-top: 16px; +} + +.menu-intro { + border-bottom: 2px solid #34343d; + padding: 20px 4px 28px; + text-align: center; +} + +.menu-intro > p:last-child { + color: var(--muted); + font-size: 23px; + margin: 15px auto 0; + max-width: 700px; +} + +.progress-summary { + align-items: center; + display: grid; + gap: 9px; + grid-template-columns: auto minmax(150px, 310px) auto; + justify-content: center; + margin-top: 20px; +} + +.progress-summary > span, +.progress-summary > small { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.experience-bar { + height: 12px; +} + +.main-menu-grid { + display: grid; + gap: 15px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 0 auto; + max-width: 930px; +} + +.roguelike-mode-grid { + display: grid; + gap: 15px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 16px; +} + +.roguelike-variant-row { + display: flex; + gap: 10px; + margin-bottom: 16px; +} + +.roguelike-option-panel { + align-items: center; + display: flex; + gap: 16px; + justify-content: space-between; +} + +.roguelike-timing-row { + display: flex; + gap: 10px; +} + +.roguelike-timing-row .text-button.active { + background: var(--gold); + color: #111216; +} + +.pvp-queue-panel { + justify-content: space-between; + margin-top: 16px; +} + +.pvp-queue-panel .text-button { + white-space: nowrap; +} + +.menu-card { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: flex; + gap: 16px; + min-height: 105px; + outline: 2px solid #42414c; + padding: 16px; + text-align: left; +} + +.menu-card:first-child { + grid-column: 1 / -1; +} + +.menu-card:hover { + outline-color: var(--gold); + transform: translateY(-2px); +} + +.menu-card > span, +.class-portrait { + align-items: center; + background: #13141a; + border: 2px solid var(--gold); + color: var(--gold); + display: flex; + flex: 0 0 58px; + font-family: 'Press Start 2P', monospace; + font-size: 19px; + height: 58px; + justify-content: center; +} + +.menu-card strong, +.menu-card small { + display: block; +} + +.menu-card strong { + font-family: 'Press Start 2P', monospace; + font-size: 11px; + margin-bottom: 9px; +} + +.menu-card small { + color: var(--muted); + font-size: 18px; + line-height: 1.05; +} + +.screen-heading { + align-items: center; + border-bottom: 2px solid #34343d; + display: flex; + justify-content: space-between; + padding-bottom: 14px; +} + +.back-button, +.text-button { + background: #15161c; + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + outline: 2px solid #464550; + padding: 9px 13px; +} + +.back-button:hover, +.text-button:hover { + color: var(--ink); + outline-color: var(--gold); +} + +.combat-header-actions { + align-items: center; + display: flex; + gap: 16px; +} + +.dungeon-card { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + display: grid; + gap: 20px; + grid-template-columns: 112px 1fr minmax(180px, 240px) auto; + margin-top: 16px; + outline: 2px solid #494754; + padding: 19px; +} + +.dungeon-art { + align-items: center; + background: linear-gradient(#632831, #24151c); + border: 3px solid #0a0b0e; + color: #ef7b66; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 20px; + height: 92px; + justify-content: center; + outline: 2px solid #8d3e45; +} + +.dungeon-art.raid-art { + background: linear-gradient(#713b16, #2d160d); + color: #ffc15b; + outline-color: #a85d20; +} + +.dungeon-card h2 { + font-size: 16px; +} + +.dungeon-card p:not(.eyebrow) { + color: var(--muted); + font-size: 18px; + margin-top: 6px; +} + +.activity-select, +.loot-toolbar label { + display: grid; + gap: 8px; +} + +.activity-select > span, +.loot-toolbar span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + text-transform: uppercase; +} + +.activity-select select, +.loot-toolbar select { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 42px; + outline: 2px solid #41404a; + padding: 0 12px; + width: 100%; +} + +.difficulty-section { + background: #1c1e25; + border: 2px solid #0a0b0e; + margin-top: 14px; + outline: 2px solid #3e3d47; + padding: 16px; +} + +.compact-difficulty-section { + display: grid; + gap: 12px; +} + +.difficulty-select-row { + align-items: center; + display: flex; + gap: 18px; + justify-content: space-between; +} + +.difficulty-select-row label { + align-items: center; + display: flex; + gap: 9px; +} + +.difficulty-select-row label > span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + text-transform: uppercase; +} + +.difficulty-select-row select { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 42px; + outline: 2px solid #41404a; + padding: 0 12px; +} + +.difficulty-summary { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + display: grid; + gap: 12px; + grid-template-columns: minmax(180px, 1fr) minmax(0, 2fr); + outline: 2px solid #41404a; + padding: 11px 13px; +} + +.difficulty-summary.locked { + opacity: 0.62; +} + +.difficulty-summary strong, +.difficulty-summary small { + display: block; +} + +.difficulty-summary strong { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 9px; +} + +.difficulty-summary small { + color: var(--muted); + font-size: 15px; + margin-top: 4px; +} + +.difficulty-summary dl { + display: grid; + gap: 8px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; +} + +.difficulty-summary dl > div { + background: #15161c; + padding: 7px 8px; +} + +.difficulty-summary dt, +.difficulty-summary dd { + margin: 0; +} + +.difficulty-summary dt { + color: var(--muted); + font-size: 13px; +} + +.difficulty-summary dd { + color: var(--gold); + font-size: 14px; +} + +.difficulty-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + margin-top: 14px; +} + +.difficulty-grid > button { + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + min-height: 230px; + outline: 2px solid #41404a; + padding: 11px; + text-align: left; +} + +.difficulty-grid > button:hover, +.difficulty-grid > button.selected { + outline-color: var(--gold); + transform: translateY(-2px); +} + +.difficulty-grid > button.locked { + filter: grayscale(0.65); + opacity: 0.62; +} + +.difficulty-grid > button.selected.locked { + outline-color: #8f6368; +} + +.difficulty-title { + align-items: center; + display: flex; + gap: 8px; +} + +.difficulty-title > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + flex: 0 0 32px; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + height: 32px; + justify-content: center; +} + +.difficulty-title strong, +.difficulty-title small { + display: block; +} + +.difficulty-title strong { + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.difficulty-title small { + color: var(--muted); + font-size: 14px; + margin-top: 3px; +} + +.difficulty-grid > button > p { + color: var(--muted); + font-size: 16px; + line-height: 1; + min-height: 50px; + margin-top: 12px; +} + +.difficulty-grid dl { + margin: 10px 0 0; +} + +.difficulty-grid dl > div { + border-top: 1px solid #3c3c46; + display: flex; + justify-content: space-between; + padding: 4px 0; +} + +.difficulty-grid dt { + color: var(--muted); + font-size: 14px; +} + +.difficulty-grid dd { + color: var(--gold); + font-size: 14px; + margin: 0; +} + +.loot-preview-section { + background: #1c1e25; + border: 2px solid #0a0b0e; + margin-top: 14px; + outline: 2px solid #3e3d47; + padding: 16px; +} + +.toggle-heading { + gap: 18px; +} + +.section-note { + color: var(--muted); + font-size: 16px; + margin-top: 10px; +} + +.loot-toolbar { + display: flex; + justify-content: flex-end; + margin-top: 12px; +} + +.loot-preview-grid { + display: grid; + gap: 11px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 14px; +} + +.loot-preview-grid > article { + background: var(--panel-light); + border: 2px solid #090a0d; + outline: 2px solid #41404a; + padding: 11px; +} + +.loot-encounter-title { + align-items: center; + border-bottom: 1px solid #3b3b45; + display: flex; + gap: 9px; + padding-bottom: 9px; +} + +.loot-encounter-title > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + flex: 0 0 31px; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + height: 31px; + justify-content: center; +} + +.loot-boss-icon { + background: #15161c; + border: 2px solid #090a0d; + display: block; + flex: 0 0 35px; + height: 35px; + object-fit: cover; + outline: 1px solid #41404a; + width: 35px; +} + +.loot-encounter-title strong, +.loot-encounter-title small { + display: block; +} + +.loot-encounter-title strong { + font-family: 'Press Start 2P', monospace; + font-size: 7px; + line-height: 1.4; +} + +.loot-encounter-title small { + color: var(--muted); + margin-top: 3px; +} + +.loot-items { + display: grid; + gap: 7px; + margin-top: 9px; +} + +.loot-items > div { + align-items: center; + background: #1a1b21; + border-left: 3px solid var(--rarity-color, #a8a3ad); + display: grid; + gap: 7px; + grid-template-columns: 29px 1fr auto; + min-height: 51px; + padding: 6px; +} + +.loot-items > div > span { + align-items: center; + color: var(--rarity-color, var(--ink)); + display: flex; + font-family: 'Press Start 2P', monospace; + justify-content: center; +} + +.loot-items strong, +.loot-items small { + display: block; +} + +.loot-items strong { + color: var(--rarity-color, var(--ink)); + font-size: 14px; +} + +.loot-items small, +.loot-items i { + color: var(--muted); + font-size: 13px; +} + +.loot-items i { + font-style: normal; +} + +.leaderboard-section { + background: #1c1e25; + border: 2px solid #0a0b0e; + margin-top: 14px; + outline: 2px solid #3e3d47; + padding: 16px; +} + +.leaderboard-table { + background: var(--panel-light); + border: 2px solid #090a0d; + margin-top: 14px; + outline: 2px solid #41404a; +} + +.leaderboard-header, +.leaderboard-row { + align-items: center; + display: grid; + gap: 10px; + grid-template-columns: 55px minmax(110px, 1.2fr) minmax(110px, 1fr) 60px 85px 80px 60px; + padding: 9px 12px; +} + +.leaderboard-header { + background: #15161c; + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.leaderboard-row { + border-top: 1px solid #3a3a44; + color: var(--ink); + font-size: 16px; +} + +.leaderboard-row:nth-child(2) { + background: #30291a; +} + +.leaderboard-row > strong { + color: var(--gold); +} + +.leaderboard-empty { + color: var(--muted); + padding: 22px; + text-align: center; +} + +.leaderboard-tabs { + display: flex; + gap: 6px; + margin-top: 12px; +} + +.leaderboard-tab { + background: #15161c; + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 6px; + outline: 2px solid #3e3d47; + padding: 8px 12px; + text-transform: uppercase; +} + +.leaderboard-tab.active { + color: var(--gold); + outline-color: var(--gold); +} + +.leaderboard-tab:hover { + color: var(--ink); +} + +.tag-row { + display: flex; + gap: 7px; + margin-top: 10px; + flex-wrap: wrap; +} + +.tag-row span { + background: #15161c; + color: var(--gold); + font-size: 15px; + padding: 3px 7px; +} + +.part-buttons { + display: flex; + gap: 8px; +} + +.part-buttons .primary-button { + white-space: nowrap; +} + +.part-buttons .primary-button.selected-part { + outline-color: #fff; + background: #f0cb79; +} + +.part-buttons .primary-button.locked { + filter: grayscale(0.65); + opacity: 0.62; + cursor: not-allowed; +} + +.primary-button { + background: var(--gold); + border: 2px solid #08090c; + color: #19150e; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #816630; + padding: 13px 17px; +} + +.primary-button:hover:not(:disabled) { + background: #f0cb79; + transform: translateY(-1px); +} + +.primary-button:disabled { + cursor: wait; + opacity: 0.6; +} + +.placeholder-panel { + align-items: center; + display: flex; + flex-direction: column; + min-height: 400px; + justify-content: center; + text-align: center; +} + +.placeholder-panel > p { + color: var(--muted); + font-size: 24px; + max-width: 620px; +} + +.placeholder-runes { + color: #4a4855; + font-family: 'Press Start 2P', monospace; + font-size: 39px; + margin-bottom: 25px; + word-spacing: 20px; +} + +.talent-preview { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + gap: 19px; + max-width: 660px; + outline: 2px solid #494754; + padding: 22px; + text-align: left; +} + +.talent-preview > span { + align-items: center; + background: #4a3720; + color: var(--gold); + display: flex; + flex: 0 0 64px; + font-family: 'Press Start 2P', monospace; + height: 64px; + justify-content: center; +} + +.talent-preview strong { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 10px; +} + +.talent-preview p { + color: var(--muted); + font-size: 20px; + margin-top: 8px; +} + +.talent-screen { + padding-bottom: 20px; +} + +.talent-toolbar { + align-items: center; + background: #20222a; + border: 2px solid #0a0b0e; + display: flex; + justify-content: space-between; + margin-top: 20px; + outline: 2px solid #42414c; + padding: 14px 17px; +} + +.talent-class-summary { + align-items: center; + display: flex; + gap: 13px; +} + +.talent-class-summary > span { + align-items: center; + background: #121319; + border: 2px solid; + display: flex; + flex: 0 0 48px; + font-family: 'Press Start 2P', monospace; + height: 48px; + justify-content: center; +} + +.talent-points { + display: grid; + text-align: right; +} + +.talent-points strong { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 22px; +} + +.talent-points span { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + margin-top: 4px; +} + +.talent-points small { + color: var(--muted); + margin-top: 3px; +} + +.talent-tree { + margin-top: 17px; +} + +.talent-tier { + align-items: stretch; + border-bottom: 1px solid #393943; + display: grid; + gap: 15px; + grid-template-columns: 115px 1fr; + padding: 16px 0; +} + +.tier-label { + align-items: flex-start; + border-right: 2px solid #353640; + display: flex; + flex-direction: column; + justify-content: center; + padding-right: 12px; +} + +.tier-label span { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 8px; +} + +.tier-label small { + color: var(--muted); + font-size: 15px; + line-height: 1; + margin-top: 8px; +} + +.tier-talents { + display: grid; + gap: 11px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.talent-node { + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + flex-direction: column; + min-height: 190px; + outline: 2px solid #41404a; + padding: 11px; +} + +.talent-node.available { + outline-color: #77623b; +} + +.talent-node.invested { + background: #26271f; + outline-color: var(--gold); +} + +.talent-node.locked:not(.invested) { + opacity: 0.62; +} + +.talent-node-header { + align-items: center; + display: flex; + gap: 9px; +} + +.talent-node-header > span { + align-items: center; + background: #15161c; + border: 1px solid #55515f; + color: var(--gold); + display: flex; + flex: 0 0 37px; + font-family: 'Press Start 2P', monospace; + height: 37px; + justify-content: center; +} + +.talent-node-header strong, +.talent-node-header small { + display: block; +} + +.talent-node-header strong { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + line-height: 1.45; +} + +.talent-node-header small { + color: var(--gold); + margin-top: 2px; +} + +.talent-node > p { + color: var(--muted); + flex: 1; + font-size: 16px; + line-height: 1; + margin-top: 10px; +} + +.rank-pips { + display: flex; + gap: 4px; + margin: 10px 0; +} + +.rank-pips i { + background: #111218; + border: 1px solid #484650; + display: block; + height: 6px; + width: 18px; +} + +.rank-pips i.filled { + background: var(--gold); + border-color: #f0ce82; +} + +.talent-node > button { + background: #17181e; + border: 1px solid #08090c; + color: var(--gold); + cursor: pointer; + min-height: 30px; + outline: 1px solid #514d40; + padding: 5px; +} + +.talent-node > button:disabled { + color: #77737e; + cursor: not-allowed; + font-size: 14px; + outline-color: #3b3a43; +} + +.talent-footer { + align-items: center; + display: flex; + justify-content: space-between; + padding-top: 18px; +} + +.talent-footer > span { + color: var(--muted); +} + +.talent-footer .text-button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.gear-summary { + align-items: center; + background: #20222a; + border: 2px solid #0a0b0e; + display: grid; + gap: 14px; + grid-template-columns: 1.6fr repeat(3, 1fr); + margin-top: 20px; + outline: 2px solid #42414c; + padding: 14px 17px; +} + +.gear-character { + align-items: center; + display: flex; + gap: 12px; +} + +.gear-character > span { + align-items: center; + background: #121319; + border: 2px solid; + display: flex; + flex: 0 0 48px; + font-family: 'Press Start 2P', monospace; + height: 48px; + justify-content: center; +} + +.gear-stat { + border-left: 1px solid #464650; + display: grid; + padding-left: 14px; +} + +.gear-stat strong { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 15px; +} + +.gear-stat span { + color: var(--muted); + font-size: 15px; + margin-top: 4px; +} + +.equipment-layout { + display: grid; + gap: 17px; + grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr); + margin-top: 18px; +} + +.equipped-panel, +.inventory-panel, +.crafting-panel, +.set-bonus-panel { + background: #1c1e25; + border: 2px solid #0a0b0e; + outline: 2px solid #3e3d47; + padding: 15px; +} + +.equipment-tabs { + display: flex; + gap: 8px; + margin-top: 18px; +} + +.equipment-tab { + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 44px; + outline: 2px solid #41404a; + padding: 8px 16px; + text-transform: uppercase; +} + +.equipment-tab.active, +.equipment-tab:hover { + color: var(--gold); + outline-color: var(--gold); +} + +.equipment-heading { + align-items: center; + display: flex; + justify-content: space-between; +} + +.equipment-heading > span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.equipment-slots { + display: grid; + gap: 8px; + margin-top: 13px; +} + +.equipment-slots > button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: grid; + gap: 9px; + grid-template-columns: 38px 1fr auto; + min-height: 62px; + outline: 2px solid #41404a; + padding: 8px; + text-align: left; +} + +.equipment-slots > button:hover { + outline-color: var(--gold); +} + +.equipment-slots > button.selected-slot { + background: #29291f; + outline-color: var(--gold); +} + +.equipment-slots > button > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + font-family: 'Press Start 2P', monospace; + height: 38px; + justify-content: center; +} + +.equipment-slots > button strong { + font-size: 14px; + color: var(--rarity-color, var(--ink)); +} + +.equipment-slots > button small { + color: var(--muted); + display: block; + margin-top: 3px; +} + +.equipment-slots .item-status i { + color: var(--green); + font-size: 14px; + font-style: normal; +} + +.inventory-list { + display: grid; + gap: 8px; + margin-top: 13px; + max-height: 442px; + overflow-y: auto; + padding: 2px; +} + +.crafting-panel, +.set-bonus-panel { + margin-top: 18px; +} + +.crafting-filter-bar { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.filter-select { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + outline: 2px solid #41404a; + padding: 8px 10px; + flex: 1; +} + +.filter-select:focus { + outline-color: var(--gold); +} + +.crafting-layout { + display: grid; + gap: 12px; + grid-template-columns: minmax(0, 1fr) minmax(280px, 0.75fr); + margin-top: 13px; +} + +.crafting-list { + display: grid; + gap: 8px; + max-height: 360px; + overflow-y: auto; + padding: 2px; +} + +.crafting-list > button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + border-top-color: var(--rarity-color, #a8a3ad); + color: var(--ink); + cursor: pointer; + display: grid; + gap: 9px; + grid-template-columns: 38px 1fr auto; + min-height: 62px; + outline: 2px solid #41404a; + padding: 8px; + text-align: left; +} + +.crafting-list > button.selected { + outline-color: var(--gold); +} + +.crafting-list > button > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + font-family: 'Press Start 2P', monospace; + height: 38px; + justify-content: center; +} + +.crafting-list strong, +.crafting-list small { + display: block; +} + +.crafting-list small { + color: var(--muted); + margin-top: 3px; +} + +.crafting-list i { + color: var(--gold); + font-size: 14px; + font-style: normal; + text-align: right; +} + +.crafting-detail { + background: var(--panel-light); + border: 2px solid #090a0d; + border-top-color: var(--rarity-color, #a8a3ad); + display: grid; + gap: 10px; + padding: 10px; +} + +.crafting-detail .item-detail { + padding: 0; + border: 0; +} + +.crafting-components { + display: grid; + gap: 6px; +} + +.crafting-components > div { + align-items: center; + background: #15161c; + display: grid; + gap: 8px; + grid-template-columns: 26px 1fr auto; + padding: 7px; +} + +.crafting-components > div.ready { + color: var(--green); +} + +.crafting-components > div.missing { + color: #e36c79; +} + +.crafting-components span { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + text-align: center; +} + +.crafting-components i { + font-style: normal; +} + +.set-bonus-list { + display: grid; + gap: 8px; + margin-top: 13px; +} + +.set-bonus-list > div { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--muted); + display: grid; + gap: 10px; + grid-template-columns: 110px 1fr auto; + outline: 2px solid #41404a; + padding: 10px; +} + +.set-bonus-list > div.active { + color: var(--ink); + outline-color: var(--gold); +} + +.set-bonus-list strong, +.set-bonus-list i { + color: var(--gold); + font-style: normal; +} + +.inventory-filter-clear { + background: #15161c; + border: 2px solid #090a0d; + color: var(--gold); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + margin-top: 12px; + outline: 2px solid #41404a; + padding: 8px 10px; +} + +.inventory-filter-clear:hover { + outline-color: var(--gold); +} + +.inventory-empty { + color: var(--muted); + padding: 28px 12px; + text-align: center; +} + +.inventory-list > button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: grid; + gap: 9px; + grid-template-columns: 38px 1fr auto; + min-height: 62px; + outline: 2px solid #41404a; + padding: 8px; + text-align: left; +} + +.inventory-list > button.selected { + outline-color: var(--gold); +} + +.inventory-list > button > span, +.item-title > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + font-family: 'Press Start 2P', monospace; + height: 38px; + justify-content: center; +} + +.inventory-list strong, +.inventory-list small { + display: block; +} + +.inventory-list small { + color: var(--muted); + margin-top: 3px; +} + +.inventory-list i { + color: var(--green); + font-size: 14px; + font-style: normal; +} + +.inventory-list .item-status { + display: grid; + gap: 3px; + justify-items: end; +} + +.inventory-list .item-quantity { + color: var(--gold); +} + +.rarity-uncommon { + --rarity-color: #63c882; +} + +.rarity-rare { + --rarity-color: #62a9ed; +} + +.rarity-epic { + --rarity-color: #b584e3; +} + +.rarity-common { + --rarity-color: #a8a3ad; +} + +.equipment-slots > button, +.inventory-list > button, +.item-detail { + border-top-color: var(--rarity-color, #a8a3ad); + border-top-width: 3px; +} + +.inventory-list > button strong, +.item-detail h2 { + color: var(--rarity-color, var(--ink)); +} + +.item-comparison { + align-items: stretch; + background: #1c1e25; + border: 2px solid #0a0b0e; + display: grid; + gap: 12px; + grid-template-columns: 1fr auto 1fr minmax(140px, 0.5fr); + margin-top: 18px; + min-height: 160px; + outline: 2px solid #3e3d47; + padding: 12px; +} + +.item-detail { + background: var(--panel-light); + border: 2px solid #090a0d; + padding: 12px; +} + +.item-title { + align-items: center; + display: flex; + gap: 10px; +} + +.item-title > span { + flex: 0 0 42px; + height: 42px; +} + +.item-title small { + color: var(--muted); +} + +.item-detail > p:not(.eyebrow) { + color: var(--muted); + font-size: 17px; + margin-top: 11px; +} + +.item-detail > p.owned-quantity { + color: var(--gold); +} + +.item-detail ul { + color: var(--ink); + list-style: none; + margin: 11px 0 0; + padding: 0; +} + +.item-detail li { + border-top: 1px solid #3a3a44; + padding: 4px 0; +} + +.empty-comparison { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.comparison-arrow { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + justify-content: center; +} + +.equip-action { + align-items: stretch; + display: flex; + flex-direction: column; + gap: 8px; + justify-content: flex-end; +} + +.discard-button { + background: #2a1a1d; + border: 2px solid #090a0d; + color: #ef8994; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #694048; + padding: 11px 8px; +} + +.discard-button:disabled { + cursor: default; + opacity: 0.55; +} + +.breakdown-button { + background: #1a2a1d; + border: 2px solid #090a0d; + color: #89ef94; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #406948; + padding: 11px 8px; +} + +.breakdown-button:disabled { + cursor: default; + opacity: 0.55; +} + +.component-note { + color: var(--muted); + font-size: 14px; + text-align: center; +} + +.comparison-delta { + display: grid; + gap: 6px; + margin-bottom: 12px; +} + +.comparison-delta span { + background: #15161c; + padding: 5px 7px; + text-align: center; +} + +.comparison-delta .positive { + color: #71d798; +} + +.comparison-delta .negative { + color: #e36c79; +} + +.equipment-footer { + color: var(--muted); + padding-top: 15px; +} + +.customize-tabs { + display: grid; + gap: 8px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 16px; +} + +.customize-tabs button { + background: #15161c; + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 44px; + outline: 2px solid #41404a; + padding: 8px 10px; +} + +.customize-tabs button.active, +.customize-tabs button:hover { + color: var(--gold); + outline-color: var(--gold); +} + +.embedded-screen .gear-summary, +.embedded-screen .talent-toolbar { + margin-top: 16px; +} + +.customize-layout { + display: grid; + gap: 18px; + grid-template-columns: 250px minmax(0, 1fr); + margin-top: 16px; +} + +.class-picker { + border-right: 2px solid #353640; + padding-right: 16px; +} + +.class-picker > button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: flex; + gap: 10px; + margin-bottom: 10px; + outline: 2px solid #41404a; + padding: 10px; + text-align: left; + width: 100%; +} + +.class-picker > button:hover, +.class-picker > button.active { + outline-color: var(--class-color); +} + +.class-picker > button > span { + align-items: center; + background: #111218; + color: var(--class-color); + display: flex; + flex: 0 0 35px; + font-family: 'Press Start 2P', monospace; + height: 35px; + justify-content: center; +} + +.class-picker strong, +.class-picker small { + display: block; +} + +.class-picker strong { + font-family: 'Press Start 2P', monospace; + font-size: 8px; +} + +.class-picker small { + color: var(--muted); + margin-top: 4px; +} + +.class-detail { + align-items: center; + background: #20222a; + border-bottom: 2px solid #3b3b45; + display: flex; + gap: 16px; + padding: 15px; +} + +.class-detail h2 { + font-size: 13px; +} + +.class-detail p:last-child { + color: var(--muted); + font-size: 18px; + margin-top: 5px; +} + +.loadout-heading, +.ability-library-heading, +.save-row { + align-items: center; + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.loadout-heading > span, +.save-row > span { + color: var(--muted); + font-size: 17px; +} + +.ability-slots { + display: grid; + gap: 8px; + grid-template-columns: repeat(6, minmax(0, 1fr)); + margin-top: 12px; +} + +.ability-slots button { + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + min-height: 92px; + outline: 2px solid #44434d; + padding: 8px 3px; + position: relative; +} + +.ability-slots button.selected { + outline-color: var(--gold); + transform: translateY(-2px); +} + +.ability-slots kbd { + color: var(--muted); + left: 5px; + position: absolute; + top: 4px; +} + +.ability-slots span, +.ability-slots strong { + display: block; +} + +.ability-slots span { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 16px; + margin-bottom: 8px; +} + +.ability-slots strong { + font-size: 14px; +} + +.ability-library { + display: grid; + gap: 8px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 11px; + max-height: 335px; + overflow-y: auto; + padding: 2px; +} + +.ability-library > button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: grid; + gap: 10px; + grid-template-columns: 38px 1fr auto; + min-height: 78px; + outline: 2px solid #44434d; + padding: 9px; + text-align: left; +} + +.ability-library > button:hover:not(:disabled) { + outline-color: var(--gold); +} + +.ability-library > button.equipped { + outline-color: var(--green); +} + +.ability-library > button.locked { + filter: grayscale(1); + opacity: 0.5; +} + +.ability-library > button > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + font-family: 'Press Start 2P', monospace; + height: 38px; + justify-content: center; +} + +.ability-library strong, +.ability-library small { + display: block; +} + +.ability-library small { + color: var(--muted); + font-size: 15px; + line-height: 1; + margin-top: 4px; +} + +.ability-library i { + color: var(--gold); + font-size: 14px; + font-style: normal; +} + +.run-progress { + display: flex; + gap: 9px; +} + +.run-progress span { + align-items: center; + background: #111218; + border: 2px solid var(--edge); + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 9px; + height: 32px; + justify-content: center; + width: 32px; +} + +.run-progress .active { + border-color: var(--gold); + color: var(--gold); +} + +.run-progress .complete { + background: var(--green); + color: white; +} + +.enemy-card { + align-items: center; + display: flex; + gap: 18px; + margin-top: 18px; + padding: 18px; +} + +.enemy-portrait { + align-items: center; + background: #481e29; + border: 3px solid #0d0e12; + color: var(--red-bright); + display: flex; + flex: 0 0 70px; + font-family: 'Press Start 2P', monospace; + font-size: 25px; + height: 70px; + justify-content: center; + outline: 2px solid var(--red); + text-shadow: 3px 3px #12070a; +} + +.enemy-portrait img { + display: block; + height: 100%; + object-fit: cover; + width: 100%; +} + +.enemy-info { + flex: 1; +} + +.enemy-info p { + color: var(--muted); + font-size: 21px; + margin-top: 8px; +} + +.bar-label { + color: var(--ink); + display: flex; + font-size: 20px; + justify-content: space-between; +} + +.bar { + background: #08090c; + border: 2px solid #090a0d; + height: 17px; + overflow: hidden; + position: relative; +} + +.bar > span, +.bar > i { + display: block; + height: 100%; + transition: width 180ms linear; +} + +.enemy-health { + margin-top: 5px; +} + +.enemy-health > span { + background: var(--red); + box-shadow: inset 0 5px #cf4b59; +} + +.combat-layout { + display: grid; + gap: 18px; + grid-template-columns: minmax(0, 1.8fr) minmax(260px, 0.8fr); + margin-top: 18px; +} + +.party-panel, +.combat-log { + min-height: 390px; + padding: 18px; +} + +.combat-side-rail { + display: flex; + flex-direction: column; + min-height: 0; +} + +.panel-heading, +.resource-row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.panel-heading > span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 8px; +} + +.party-grid { + display: grid; + gap: 11px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 17px; +} + +.party-member { + background: var(--panel-light); + border: 2px solid #0a0b0e; + color: var(--ink); + cursor: pointer; + min-height: 92px; + outline: 2px solid #3a3944; + padding: 10px; + position: relative; + text-align: left; +} + +.party-member:first-child { + grid-column: 1 / -1; +} + +.raid-party-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.raid-party-grid .party-member:first-child { + grid-column: auto; +} + +.party-member:hover { + outline-color: var(--gold); + transform: translateY(-1px); +} + +.party-member.selected { + background: #29291f; + border-color: #f1ca68; + box-shadow: + inset 0 0 0 2px #6e5727, + 0 0 0 3px #17140c, + 0 0 16px rgba(229, 185, 95, 0.72); + outline: 3px solid var(--gold); + transform: translateY(-2px); +} + +.party-member.selected .member-header strong { + color: #ffe29a; + text-shadow: 2px 2px #171107; +} + +.party-member.selected .member-health { + border-color: #d9b55a; +} + +.target-marker { + align-items: center; + background: var(--gold); + border: 2px solid #0a0b0e; + color: #21180a; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 5px; + padding: 5px 7px; + position: absolute; + right: 7px; + text-transform: uppercase; + top: -12px; + z-index: 2; +} + +.target-marker i { + border-bottom: 4px solid transparent; + border-left: 6px solid #21180a; + border-top: 4px solid transparent; + height: 0; + width: 0; +} + +.party-member.dead { + filter: grayscale(1); + opacity: 0.48; +} + +.party-member.dead.selected { + filter: grayscale(0.35); + opacity: 0.72; +} + +.party-member > small { + display: none; +} + +.member-header { + align-items: center; + display: flex; + gap: 8px; +} + +.member-header strong { + flex: 1; + min-width: 0; +} + +.member-header small { + color: var(--muted); + flex: 0 0 auto; + font-size: 15px; + text-align: right; +} + +.party-panel-top { + margin-bottom: 12px; +} + +.role { + align-items: center; + border: 1px solid white; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + height: 19px; + justify-content: center; + width: 19px; +} + +.role-tank { + background: var(--blue); +} + +.role-healer { + background: var(--green); +} + +.role-damage { + background: var(--red); +} + +.member-health { + margin-top: 8px; +} + +.member-health > span { + background: var(--green); + box-shadow: inset 0 4px #63bf82; +} + +.member-health > i { + background: rgba(85, 174, 231, 0.72); + position: absolute; + right: 0; + top: 0; +} + +.member-effects { + display: flex; + flex-wrap: wrap; + gap: 5px; + min-height: 18px; + margin-top: 6px; +} + +.member-effects span { + font-size: 14px; + padding: 0 5px; +} + +.floating-combat-texts { + inset: 0; + pointer-events: none; + position: absolute; +} + +.floating-heal { + animation: floating-heal 0.9s ease-out forwards; + color: #91f0b0; + font-family: 'Press Start 2P', monospace; + font-size: 9px; + left: 50%; + position: absolute; + text-shadow: 1px 1px #102016; + top: 28px; + transform: translateX(-50%); + white-space: nowrap; +} + +@keyframes floating-heal { + 0% { + opacity: 0; + transform: translate(-50%, 6px) scale(0.85); + } + + 18% { + opacity: 1; + transform: translate(-50%, 0) scale(1); + } + + 100% { + opacity: 0; + transform: translate(-50%, -20px) scale(1); + } +} + +.buff { + background: #244c36; + color: #77dfa0; +} + +.debuff { + background: #5c2028; + color: #ff8290; +} + +.combat-log ol { + list-style: none; + margin: 17px 0 0; + padding: 0; +} + +.combat-log li { + border-bottom: 1px solid #30313a; + color: var(--muted); + font-size: 17px; + line-height: 1.1; + padding: 8px 2px; +} + +.combat-log .heal { + color: #72d99b; +} + +.combat-log .danger { + color: #f16d7b; +} + +.combat-log .loot { + color: var(--gold); +} + +.action-panel { + margin-top: 18px; + padding: 12px 18px; +} + +.resource-row strong { + color: var(--ink); + font-size: 20px; +} + +.active-target-card { + align-items: center; + background: #29291f; + border: 2px solid var(--gold); + box-shadow: inset 0 0 0 2px #5f4d28; + display: flex; + gap: 11px; + min-width: 300px; + padding: 9px 12px; +} + +.active-target-card > .role { + flex: 0 0 30px; + font-size: 9px; + height: 30px; + width: 30px; +} + +.active-target-card strong, +.active-target-card small { + display: block; +} + +.active-target-card strong { + color: #ffe29a; +} + +.active-target-card small { + color: var(--muted); + margin-top: 3px; +} + +.mana-wrap { + color: #82bfff; + font-size: 17px; + text-align: right; + width: min(360px, 50%); +} + +.party-mana-wrap { + text-align: left; + width: 100%; +} + +.party-mana-wrap > span { + display: block; + margin-bottom: 4px; +} + +.action-panel .resource-row { + justify-content: flex-start; +} + +.mana-bar { + margin-top: 4px; +} + +.mana-bar > span { + background: var(--blue); + box-shadow: inset 0 4px #60a9ed; +} + +.spell-bar { + display: grid; + gap: 11px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + margin-top: 16px; +} + +.spell-bar.six-slots { + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + +.vertical-spell-bar, +.vertical-spell-bar.six-slots { + align-content: start; + flex: 1; + grid-template-columns: 1fr; + margin-top: 16px; + overflow-x: hidden; + overflow-y: auto; + padding-right: 4px; +} + +.vertical-spell-bar .spell { + min-height: 64px; + padding: 8px 8px 8px 56px; +} + +.vertical-spell-bar .spell-icon { + font-size: 12px; + height: 30px; + left: 10px; + margin: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 30px; +} + +.vertical-spell-bar .spell strong { + font-size: 14px; +} + +.vertical-spell-bar .spell small { + font-size: 12px; +} + +.vertical-spell-bar .spell kbd { + left: auto; + right: 4px; +} + +.empty-spell { + align-items: center; + color: #5e5b68; + display: flex; + justify-content: center; +} + +.spell { + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + min-height: 112px; + outline: 2px solid #494756; + padding: 8px 5px; + position: relative; +} + +.spell:hover:not(:disabled) { + outline-color: var(--gold); + transform: translateY(-2px); +} + +.spell:disabled { + cursor: not-allowed; + filter: grayscale(0.8); + opacity: 0.48; +} + +.spell kbd { + align-items: center; + background: #090a0d; + color: var(--muted); + display: inline-flex; + font-size: 11px; + justify-content: center; + left: 4px; + min-height: 24px; + max-width: calc(100% - 8px); + overflow: hidden; + padding: 2px 6px; + position: absolute; + text-overflow: ellipsis; + white-space: nowrap; + top: 4px; +} + +.spell-icon { + align-items: center; + background: #5f4124; + border: 2px solid #0a0b0e; + color: #ffe6a7; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 17px; + height: 42px; + justify-content: center; + margin: 1px auto 7px; + width: 42px; +} + +.spell-group { + background: #694e24; +} + +.spell-shield { + background: #255b78; +} + +.spell-cleanse { + background: #573c75; +} + +.spell strong, +.spell small { + display: block; +} + +.spell strong { + font-size: 16px; +} + +.spell small { + color: var(--muted); +} + +.spell > i { + align-items: center; + background: rgba(7, 8, 11, 0.84); + display: flex; + font-style: normal; + inset: 0; + justify-content: center; + position: absolute; +} + +.result-log { + background: #17181e; + border: 2px solid #08090c; + margin-top: 14px; + max-height: 240px; + overflow-y: auto; + padding: 8px; + text-align: left; +} + +.log-entry { + border-bottom: 1px solid #30313a; + color: var(--muted); + font-size: 16px; + line-height: 1.2; + padding: 7px 2px; +} + +.log-entry:last-child { + border-bottom: 0; +} + +.log-entry.heal { + color: #72d99b; +} + +.log-entry.danger { + color: #f16d7b; +} + +.log-entry.loot { + color: var(--gold); +} + +.result-screen, +.pause-screen { + align-items: center; + background: rgba(5, 5, 8, 0.88); + display: flex; + inset: 0; + justify-content: center; + position: fixed; + z-index: 10; +} + +.result-screen > div, +.pause-screen > div { + background: var(--panel); + border: 3px solid #0b0c0f; + box-shadow: 8px 8px 0 #050507; + max-width: 520px; + outline: 2px solid var(--gold); + padding: 32px; + text-align: center; +} + +.result-screen p:not(.eyebrow), +.pause-screen p:not(.eyebrow) { + color: var(--muted); + font-size: 22px; + margin-top: 15px; +} + +.reward-summary { + margin-top: 14px; +} + +.reward-summary > p { + margin-top: 8px !important; +} + +.reward-summary .level-gain { + color: var(--gold) !important; + font-family: 'Press Start 2P', monospace; + font-size: 12px !important; +} + +.level-gain small { + color: #d8c79d; + display: block; + font-family: 'VT323', monospace; + font-size: 18px; + margin-top: 9px; +} + +.reward-summary .ability-unlock { + align-items: center; + background: #213c2b; + border: 1px solid #4f9c68; + color: #83dea2 !important; + display: flex; + font-size: 18px !important; + gap: 9px; + justify-content: center; + padding: 7px; +} + +.reward-summary .efficiency-result small { + color: var(--muted); + display: block; + margin-top: 3px; +} + +.ability-unlock span { + color: var(--gold); + font-family: 'Press Start 2P', monospace; +} + +.reward-summary .reward-error { + color: #ff8190 !important; +} + +.run-loot-rolls { + display: grid; + gap: 6px; + margin-top: 12px; +} + +.run-loot-rolls > div { + align-items: center; + background: #17181e; + border-left: 3px solid #55515e; + column-gap: 10px; + display: grid; + font-size: 15px; + grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); + padding: 6px 9px; + text-align: left; +} + +.run-loot-rolls > div.dropped { + border-left-color: var(--gold); +} + +.run-loot-rolls strong { + color: var(--muted); +} + +.run-loot-rolls span { + color: var(--ink); + text-align: right; +} + +.run-loot-rolls > small { + color: var(--muted); +} + +.upgrade-choice-grid { + display: grid; + gap: 10px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 16px; +} + +.upgrade-choice-grid button { + background: #20232c; + border: 2px solid #08090c; + color: var(--ink); + cursor: pointer; + margin-top: 0; + min-height: 128px; + min-width: 0; + outline: 2px solid #4d4c58; + padding: 12px; + text-align: left; +} + +.upgrade-choice-grid button:hover, +.upgrade-choice-grid button:focus-visible { + outline-color: var(--gold); +} + +.upgrade-choice-grid strong, +.upgrade-choice-grid small { + display: block; +} + +.upgrade-choice-grid strong { + color: var(--gold); + font-size: 10px; + line-height: 1.5; + overflow-wrap: anywhere; +} + +.upgrade-choice-grid .selected-upgrade, +.selected-upgrade { + outline-color: var(--gold); +} + +.upgrade-choice-grid small, +.roguelike-upgrade-list { + color: var(--muted); + font-size: 15px; + line-height: 1.25; + margin-top: 10px; + overflow-wrap: anywhere; +} + +.bonus-item { + border-top: 2px solid var(--gold); + margin-top: 14px; + padding-top: 14px; +} + +.bonus-item-detail { + align-items: center; + display: flex; + gap: 10px; + margin-top: 8px; +} + +.bonus-item-detail > span { + align-items: center; + background: #4a3720; + color: var(--gold); + display: flex; + flex: 0 0 38px; + font-family: 'Press Start 2P', monospace; + height: 38px; + justify-content: center; +} + +.bonus-item-detail strong { + color: var(--rarity-color, var(--ink)); + font-size: 16px; +} + +.bonus-item-detail small { + color: var(--muted); +} + +.pvp-match-screen { + gap: 16px; +} + +.pvp-board { + display: grid; + gap: 16px; + grid-template-columns: minmax(0, 1fr) minmax(280px, 0.8fr) minmax(0, 1fr); +} + +.pvp-side, +.pvp-middle-panel { + gap: 12px; +} + +.pvp-vertical-spell-bar, +.pvp-vertical-spell-bar.six-slots { + grid-template-columns: 1fr; +} + +.pvp-vertical-spell-bar .spell { + min-height: 86px; +} + +.pvp-screen-tools { + align-items: center; + display: flex; + gap: 12px; +} + +.pvp-resource-row { + justify-content: flex-end; +} + +.pvp-resource-wrap { + color: #82bfff; + min-width: 220px; + text-align: right; + width: min(240px, 100%); +} + +.pvp-resource-wrap > span { + display: block; + margin-bottom: 4px; +} + +.pvp-side .party-member, +.pvp-side .party-member > div, +.pvp-side .party-member > small { + min-width: 0; +} + +.pvp-enemy-race { + display: grid; + gap: 12px; +} + +.pvp-choice-columns { + display: grid; + gap: 16px; + grid-template-columns: 1fr; + margin-top: 16px; +} + +.pvp-choice-columns > div > strong { + display: block; + margin-bottom: 8px; +} + +.pvp-choice-columns .upgrade-choice-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.pvp-choice-columns .upgrade-choice-grid button { + background: #252833; + min-height: 120px; + padding: 14px; +} + +.pvp-leaderboard-row { + grid-template-columns: 88px minmax(0, 1fr) 90px 90px 90px; +} + +.pvp-upgrade-dialog { + max-width: 1120px !important; + text-align: left !important; + width: min(1120px, calc(100vw - 32px)); +} + +.pvp-upgrade-dialog .upgrade-choice-grid strong { + color: #ffe8a5; + font-size: 11px; + line-height: 1.6; +} + +.pvp-upgrade-dialog .upgrade-choice-grid small { + color: #d3d9e6; + font-size: 16px; + line-height: 1.35; +} + +.pvp-upgrade-dialog .upgrade-choice-grid button.selected-upgrade { + background: #303427; + box-shadow: inset 0 0 0 2px #6e5727; +} + +.result-screen button, +.pause-screen button { + background: var(--gold); + border: 2px solid #08090c; + color: #19150e; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 9px; + margin-top: 22px; + outline: 2px solid #816630; + padding: 13px 18px; +} + +.result-screen .secondary-result-button, +.pause-screen .secondary-result-button { + background: #252630; + color: var(--ink); + margin-left: 10px; + outline-color: #4d4c58; +} + +.pause-screen button { + display: block; + width: 100%; +} + +.pause-screen .secondary-result-button { + margin-left: 0; + margin-top: 12px; +} + +.pause-screen button:focus-visible { + outline: 3px solid #fff4a8 !important; + box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65); + z-index: 1; +} + +@media (max-height: 720px) { + .game-shell { + margin: 6px auto; + width: min(1180px, calc(100% - 20px)); + } + + .topbar { + padding: 9px 14px; + } + + .app-header { + min-height: 56px; + } + + .character-summary { + gap: 9px; + } + + .character-summary strong { + font-size: 17px; + } + + .menu-screen, + .content-screen, + .message-panel { + margin-top: 8px; + padding: 14px; + } + + .main-menu-grid { + gap: 8px; + } + + .menu-card { + min-height: 72px; + padding: 10px; + } + + .menu-card > span, + .class-portrait { + flex-basis: 44px; + font-size: 14px; + height: 44px; + } + + .menu-card strong { + margin-bottom: 4px; + } + + .menu-card small { + font-size: 15px; + } + + .screen-heading { + padding-bottom: 10px; + } + + .screen-heading .eyebrow { + display: none; + } + + .screen-heading h1 { + font-size: 16px; + } + + .dungeon-card { + gap: 12px; + grid-template-columns: 72px 1fr; + margin-top: 10px; + padding: 9px 10px; + } + + .activity-select, + .part-buttons { + grid-column: 1 / -1; + } + + .dungeon-art { + font-size: 15px; + height: 62px; + } + + .dungeon-card p:not(.eyebrow) { + font-size: 16px; + line-height: 1; + } + + .tag-row { + gap: 5px; + margin-top: 6px; + } + + .tag-row span { + font-size: 14px; + padding: 2px 6px; + } + + .part-buttons { + flex-direction: column; + gap: 5px; + } + + .part-buttons .primary-button, + .text-button, + .back-button { + padding: 7px 10px; + } + + .difficulty-section, + .loot-preview-section, + .leaderboard-section { + margin-top: 8px; + padding: 8px; + } + + .equipment-heading .eyebrow { + display: none; + } + + .equipment-heading h2 { + font-size: 11px; + } + + .difficulty-select-row select { + min-height: 36px; + } + + .difficulty-summary { + padding: 8px 10px; + } + + .difficulty-summary small { + font-size: 14px; + } + + .difficulty-summary dl > div { + padding: 5px 6px; + } + + .customize-tabs { + margin-top: 10px; + } + + .customize-layout, + .embedded-screen .gear-summary, + .embedded-screen .talent-toolbar { + margin-top: 10px; + } + + .customize-screen { + display: flex; + flex-direction: column; + height: calc(100dvh - 76px); + overflow: hidden; + } + + .customize-screen > .customize-layout, + .customize-screen > .embedded-screen { + flex: 1; + min-height: 0; + overflow-y: auto; + } + + .customize-layout { + grid-template-columns: 210px minmax(0, 1fr); + } + + .class-picker, + .loadout-editor { + min-height: 0; + } + + .ability-slots button { + min-height: 76px; + } + + .ability-library { + max-height: 190px; + } +} + +@media (max-width: 800px) { + .auth-panel { + grid-template-columns: 1fr; + } + + .auth-brand { + min-height: 260px; + } + + .combat-layout { + grid-template-columns: 1fr; + } + + .party-grid, + .spell-bar, + .spell-bar.six-slots, + .main-menu-grid, + .roguelike-mode-grid, + .upgrade-choice-grid, + .ability-library { + grid-template-columns: 1fr; + } + + .party-member:first-child, + .menu-card:first-child { + grid-column: auto; + } + + .spell { + min-height: 92px; + } + + .resource-row { + align-items: stretch; + flex-direction: column; + gap: 12px; + } + + .roguelike-variant-row, + .roguelike-option-panel, + .roguelike-timing-row, + .pvp-screen-tools { + align-items: stretch; + flex-direction: column; + } + + .pvp-board, + .pvp-choice-columns, + .pvp-choice-columns .upgrade-choice-grid { + grid-template-columns: 1fr; + } + + .active-target-card, + .mana-wrap { + width: 100%; + } + + .active-target-card { + min-width: 0; + } + + .customize-layout { + grid-template-columns: 1fr; + } + + .talent-tier { + grid-template-columns: 1fr; + } + + .tier-label { + border-bottom: 2px solid #353640; + border-right: 0; + padding-bottom: 9px; + } + + .tier-talents { + grid-template-columns: 1fr; + } + + .talent-node { + grid-column: auto !important; + } + + .gear-summary, + .equipment-layout, + .crafting-layout { + grid-template-columns: 1fr; + } + + .item-comparison { + grid-template-columns: 1fr; + min-height: 0; + } + + .set-bonus-list > div { + grid-template-columns: 1fr; + } + + .character-summary { + flex-wrap: wrap; + justify-content: flex-end; + } + + .difficulty-select-row, + .toggle-heading { + align-items: stretch; + flex-direction: column; + } + + .difficulty-select-row label { + align-items: stretch; + flex-direction: column; + } + + .difficulty-summary { + grid-template-columns: 1fr; + } + + .difficulty-summary dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .difficulty-grid { + grid-template-columns: 1fr; + } + + .leaderboard-table { + overflow-x: auto; + } + + .leaderboard-header, + .leaderboard-row { + min-width: 760px; + } + + .difficulty-grid > button { + min-height: auto; + } + + .loot-preview-grid { + grid-template-columns: 1fr; + } + + .gear-stat { + border-left: 0; + border-top: 1px solid #464650; + padding-left: 0; + padding-top: 8px; + } + + .comparison-arrow { + padding: 2px 0; + } + + .class-picker { + border-bottom: 2px solid #353640; + border-right: 0; + padding-bottom: 10px; + padding-right: 0; + } + + .ability-slots { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dungeon-card { + grid-template-columns: 1fr; + } + + .progress-summary { + grid-template-columns: 1fr; + } + + .settings-heading, + .settings-footer { + align-items: flex-start; + flex-direction: column; + gap: 12px; + } + + .dual-screen-settings { + grid-template-columns: 1fr; + } + + .dual-screen-settings > small { + grid-column: auto; + } + + .android-display-list { + grid-column: auto; + } + + .dual-screen-actions { + min-width: 0; + } + + .controller-icon-options { + grid-template-columns: 1fr; + } + + .binding-list { + grid-template-columns: 1fr; + } + + .controller-keyboard { + max-height: calc(100vh - 24px); + overflow-y: auto; + } + + .controller-keyboard-grid { + grid-template-columns: repeat(6, 1fr); + } + + .controller-keyboard-actions { + grid-template-columns: 1fr 1fr; + } + + .target-controls { + align-items: flex-end; + flex-direction: column; + gap: 7px; + } + + .dual-bottom-status { + grid-template-columns: 1fr; + } + + .dual-bottom-actions { + min-height: 0; + padding: 18px; + } + + .dual-bottom-actions .active-target-card, + .dual-bottom-actions .mana-wrap { + min-width: 0; + width: 100%; + } + + .dual-bottom-actions .spell { + min-height: 125px; + } +} + +/* ─── Admin Panel ─── */ +.admin-tabs { + display: flex; + gap: 8px; + margin: 16px 0; +} + +.admin-tab { + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--muted); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #494754; + padding: 10px 18px; + text-transform: uppercase; +} + +.admin-tab.active { + background: var(--gold); + color: #19150e; + outline-color: #816630; +} + +.admin-tab:hover:not(.active) { + color: var(--ink); + outline-color: var(--gold); +} + +.admin-panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +.admin-search { + background: #111217; + border: 2px solid #090a0d; + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #3e3d47; + padding: 10px 12px; + width: 100%; +} + +.admin-search:focus { + outline-color: var(--gold); +} + +.admin-group-header { + color: var(--gold); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 10px; + margin: 12px 0 8px; + padding: 8px 4px; + text-transform: uppercase; + user-select: none; +} + +.admin-group-header:hover { + color: #f0cb79; +} + +.admin-grid { + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); +} + +.admin-card { + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + flex-direction: column; + gap: 8px; + outline: 2px solid #494754; + padding: 12px; +} + +.boss-image-grid { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.boss-image-card { + align-items: start; +} + +.boss-image-card img { + aspect-ratio: 1; + background: #481e29; + border: 2px solid #090a0d; + display: block; + object-fit: cover; + outline: 2px solid #7a2940; + width: 84px; +} + +.boss-upload-button { + background: #15161c; + border: 2px solid #090a0d; + color: var(--gold); + cursor: pointer; + display: inline-flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + outline: 2px solid #41404a; + padding: 8px 10px; + text-transform: uppercase; +} + +.boss-upload-button input { + display: none; +} + +.boss-upload-button:has(input:disabled) { + color: var(--muted); + cursor: wait; +} + +.admin-item-header { + align-items: center; + display: flex; + gap: 10px; +} + +.admin-glyph { + font-family: 'Press Start 2P', monospace; + font-size: 16px; + height: 32px; + line-height: 32px; + text-align: center; + width: 32px; +} + +.admin-item-meta { + color: var(--muted); + display: block; + font-size: 7px; + margin-top: 2px; +} + +.admin-item-desc { + color: var(--muted); + font-size: 11px; + line-height: 1.4; + margin: 0; +} + +.admin-item-stats { + display: flex; + gap: 16px; +} + +.admin-item-stats > span { + color: var(--green); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.admin-edit-form { + display: flex; + flex-direction: column; + gap: 8px; +} + +.admin-edit-form label { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 8px; + text-transform: uppercase; +} + +.admin-edit-form input, +.admin-edit-form select, +.admin-edit-form textarea { + background: #111217; + border: 2px solid #090a0d; + color: var(--ink); + flex: 1; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + outline: 2px solid #3e3d47; + padding: 6px 8px; +} + +.admin-edit-form textarea { + min-height: 48px; + resize: vertical; +} + +.admin-edit-form input:focus, +.admin-edit-form select:focus, +.admin-edit-form textarea:focus { + outline-color: var(--gold); +} + +.admin-edit-actions { + display: flex; + gap: 8px; + margin-top: 4px; +} + +/* Loot tab */ +.admin-loot-selectors { + display: flex; + gap: 16px; +} + +.admin-loot-selectors label { + align-items: center; + color: var(--muted); + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 8px; + text-transform: uppercase; +} + +.admin-loot-selectors select { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 36px; + outline: 2px solid #41404a; + padding: 0 10px; +} + +.admin-loot-title { + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 9px; + margin: 8px 0; +} + +.admin-empty { + color: var(--muted); + font-size: 16px; +} + +.admin-loot-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.admin-loot-row { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + gap: 10px; + outline: 2px solid #494754; + padding: 8px 12px; +} + +.admin-loot-name { + flex: 1; + font-size: 14px; +} + +.admin-loot-weight { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.admin-loot-chance { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 7px; +} + +.admin-qty-input { + background: #111217; + border: 2px solid #090a0d; + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + outline: 2px solid #3e3d47; + padding: 4px 6px; + width: 50px; +} + +.admin-qty-input:focus { + outline-color: var(--gold); +} + +.danger-button { + background: var(--red); + border: 2px solid #08090c; + color: #fff; + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #7a2940; + padding: 8px 12px; +} + +.danger-button:hover { + background: var(--red-bright); +} + +.admin-add-section { + margin-top: 12px; +} + +.admin-add-section summary { + color: var(--gold); + cursor: pointer; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + margin-bottom: 8px; + text-transform: uppercase; +} + +.admin-add-section summary:hover { + color: #f0cb79; +} + +.admin-add-form { + align-items: end; + display: flex; + gap: 12px; +} + +.admin-add-form label { + color: var(--muted); + display: flex; + flex-direction: column; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 4px; + text-transform: uppercase; +} + +.admin-add-form select, +.admin-add-form input { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + outline: 2px solid #41404a; + padding: 8px 10px; +} + +.admin-add-form select:focus, +.admin-add-form input:focus { + outline-color: var(--gold); +} + +.admin-crafting-filters { + align-items: end; + display: grid; + gap: 12px; + grid-template-columns: minmax(120px, 0.5fr) minmax(180px, 0.8fr) minmax(260px, 1.5fr); +} + +.admin-crafting-filters label, +.admin-recipe-selector { + align-items: stretch; + color: var(--muted); + display: flex; + flex-direction: column; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 8px; + text-transform: uppercase; +} + +.admin-crafting-filters select, +.admin-recipe-selector select { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + flex: 1; + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-height: 36px; + outline: 2px solid #41404a; + padding: 0 10px; +} + +.admin-recipe-header { + align-items: center; + display: flex; + gap: 10px; + margin: 8px 0; + padding: 12px; + background: var(--panel-light); + border: 2px solid #090a0d; + outline: 2px solid #494754; +} + +.admin-recipe-image { + aspect-ratio: 1; + background: #1c1e25; + border: 2px solid #090a0d; + display: block; + object-fit: cover; + outline: 2px solid #41404a; + width: 64px; +} + +.admin-inline-field { + color: var(--muted); + display: flex; + flex-direction: column; + font-family: 'Press Start 2P', monospace; + font-size: 7px; + gap: 6px; + text-transform: uppercase; +} + +.admin-inline-field input { + background: #111217; + border: 2px solid #090a0d; + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-width: 230px; + outline: 2px solid #3e3d47; + padding: 8px 10px; +} + +.admin-recipe-actions { + display: flex; + gap: 10px; + margin-left: auto; +} + +@media (max-width: 800px) { + .admin-crafting-filters { + grid-template-columns: 1fr; + } + + .admin-recipe-header, + .admin-recipe-actions { + align-items: stretch; + flex-direction: column; + } + + .admin-recipe-actions { + margin-left: 0; + } +} + +.admin-rename-input { + background: #111217; + border: 2px solid #090a0d; + color: var(--ink); + font-family: 'Press Start 2P', monospace; + font-size: 8px; + min-width: 180px; + outline: 2px solid #3e3d47; + padding: 6px 8px; + width: 100%; +} + +.admin-rename-input:focus { + outline-color: var(--gold); +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b9ea69d --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,795 @@ +import { useEffect, useState } from 'react' +import './App.css' +import { CombatScreen } from './components/CombatScreen' +import { AuthScreen } from './components/AuthScreen' +import { CustomizeScreen } from './components/CustomizeScreen' +import { EquipmentScreen } from './components/EquipmentScreen' +import { PvPRoguelikeScreen } from './components/PvpRoguelikeScreen' +import { TalentScreen } from './components/TalentScreen' +import { SettingsScreen } from './components/SettingsScreen' +import { + loadCpuPvpLeaderboard, + type CpuPvpLeaderboardEntry, + type PvpContentType, +} from './pvpRoguelike' +import { + loadAuthSession, + logoutAccount, + type Account, + type AuthSession, + type CharacterProfile, +} from './profile' +import { getGameMode, type GameMode } from './gameRepository' +import { focusFirstControl } from './input.tsx' + +type Screen = + | 'menu' + | 'dungeons' + | 'combat' + | 'raids' + | 'roguelike' + | 'pvp' + | 'customize' + | 'equipment' + | 'talents' + | 'settings' + +const MENU_ITEMS: Array<{ + screen: Screen + label: string + glyph: string + description: string +}> = [ + { screen: 'dungeons', label: 'Dungeons', glyph: 'D', description: 'Guide a five-player party through dangerous encounters.' }, + { screen: 'raids', label: 'Raids', glyph: 'R', description: 'Guide a ten-player party through three-phase challenges.' }, + { screen: 'roguelike', label: 'Roguelike', glyph: 'L', description: 'Draft upgrades through escalating random encounters.' }, + { screen: 'pvp', label: 'PvP', glyph: 'P', description: 'Race another healer through roguelike encounters with buffs and sabotage.' }, + { screen: 'customize', label: 'Customize Character', glyph: 'C', description: 'Choose your class and prepare a six-ability loadout.' }, + { screen: 'settings', label: 'Settings', glyph: 'S', description: 'Remap PC and controller inputs.' }, +] + +const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty' + +function activityInitials(name: string) { + return name + .split(/\s+/) + .filter((word) => /^[A-Za-z0-9]/.test(word)) + .slice(0, 2) + .map((word) => word[0].toUpperCase()) + .join('') +} + +type RoguelikeUpgradeTiming = 'boss' | 'encounter' +type RoguelikeVariant = 'pve' | 'pvp' +type RoguelikeAbilityLabelMode = 'ability' | 'slot' + +function App() { + const [screen, setScreen] = useState('menu') + const [account, setAccount] = useState(null) + const [profile, setProfile] = useState(null) + const [authChecked, setAuthChecked] = useState(false) + const [gameMode, setGameMode] = useState(getGameMode()) + const [serverMessage, setServerMessage] = useState('') + const [selectedDifficultyId, setSelectedDifficultyId] = useState(() => { + const saved = Number(window.localStorage.getItem(LAST_DIFFICULTY_KEY)) + return Number.isFinite(saved) && saved > 0 ? saved : 1 + }) + const [selectedDungeonId, setSelectedDungeonId] = useState(1) + const [selectedRaidId, setSelectedRaidId] = useState(2) + const [roguelikeKind, setRoguelikeKind] = useState<'dungeon' | 'raid'>('dungeon') + const [roguelikeVariant, setRoguelikeVariant] = useState('pve') + const [roguelikeUpgradeTiming, setRoguelikeUpgradeTiming] = useState('encounter') + const [roguelikeAbilityLabelMode, setRoguelikeAbilityLabelMode] = useState('ability') + const [pvpContentType, setPvpContentType] = useState('dungeon') + const [selectedPart, setSelectedPart] = useState(1) + const [combatContentId, setCombatContentId] = useState(1) + const [leaderboardCategory, setLeaderboardCategory] = useState<'part_1' | 'part_2' | 'part_3' | 'full_run'>('part_1') + const [showLoot, setShowLoot] = useState(false) + const [lootSort, setLootSort] = useState<'sequence' | 'boss'>('sequence') + const [showLeaderboard, setShowLeaderboard] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + loadAuthSession() + .then((session) => { + setAccount(session.account) + setProfile(session.profile) + }) + .catch((reason: unknown) => { + setServerMessage( + reason instanceof Error + ? `${reason.message} Offline play is still available.` + : 'Unable to reach the server. Offline play is still available.', + ) + }) + .finally(() => setAuthChecked(true)) + }, []) + + useEffect(() => { + if (screen === 'combat') return + window.requestAnimationFrame(() => { + focusFirstControl() + }) + }, [screen]) + + useEffect(() => { + window.localStorage.setItem(LAST_DIFFICULTY_KEY, String(selectedDifficultyId)) + }, [selectedDifficultyId]) + + const [cpuLeaderboard, setCpuLeaderboard] = useState([]) + + useEffect(() => { + setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType)) + }, [pvpContentType, screen, roguelikeVariant]) + + function acceptSession(session: AuthSession) { + setAccount(session.account) + setProfile(session.profile) + setGameMode(getGameMode()) + setScreen('menu') + setError('') + setServerMessage('') + } + + async function signOut() { + try { + await logoutAccount() + setAccount(null) + setProfile(null) + setGameMode(getGameMode()) + setScreen('menu') + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Unable to sign out.') + } + } + + if (error) { + return ( +
+
+

Database Error

+

Character Unavailable

+

{error}

+
+
+ ) + } + + if (!authChecked) { + return ( +
+
+

Opening Chronicle

+

Loading...

+
+
+ ) + } + + if (!account || !profile) { + return ( + + ) + } + + if (screen === 'combat') { + const dungeon = combatContentId < 0 + ? profile.dungeons.find((candidate) => candidate.contentType === roguelikeKind) ?? profile.dungeons[0] + : profile.dungeons.find((candidate) => candidate.id === combatContentId) ?? profile.dungeons[0] + const difficulty = dungeon.difficulties.find( + (candidate) => candidate.id === selectedDifficultyId, + ) ?? dungeon.difficulties[0] + const roguelikePool = profile.dungeons + .filter((candidate) => candidate.contentType === roguelikeKind) + .flatMap((candidate) => candidate.encounters) + const startPart = selectedPart + return ( + { + setScreen(combatContentId < 0 ? 'roguelike' : dungeon.contentType === 'raid' ? 'raids' : 'dungeons') + }} + onProfileUpdated={setProfile} + /> + ) + } + + if (screen === 'pvp') { + const pvpPool = profile.dungeons + .filter((candidate) => candidate.contentType === pvpContentType) + .flatMap((candidate) => candidate.encounters) + return ( + { + setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType)) + setRoguelikeVariant('pvp') + setScreen('roguelike') + }} + onProfileUpdated={setProfile} + profile={profile} + /> + ) + } + + const levelStart = profile.character.currentLevelExperience + const levelEnd = profile.character.nextLevelExperience + const experienceIntoLevel = profile.character.experience - levelStart + const experienceForLevel = Math.max(1, levelEnd - levelStart) + const experiencePercent = profile.character.level >= profile.maxLevel + ? 100 + : Math.min(100, (experienceIntoLevel / experienceForLevel) * 100) + const dungeonOptions = profile.dungeons.filter((candidate) => candidate.contentType === 'dungeon') + const raidOptions = profile.dungeons.filter((candidate) => candidate.contentType === 'raid') + const dungeon = dungeonOptions.find((candidate) => candidate.id === selectedDungeonId) + ?? dungeonOptions[0]! + const raid = raidOptions.find((candidate) => candidate.id === selectedRaidId) + ?? raidOptions[0] + const activity = screen === 'raids' && raid ? raid : dungeon + const activityOptions = screen === 'raids' ? raidOptions : dungeonOptions + const selectedDifficulty = activity.difficulties.find( + (candidate) => candidate.id === selectedDifficultyId, + ) ?? activity.difficulties[0] + const difficultyLocked = profile.character.level < selectedDifficulty.unlockLevel + const completedSections = activity.contentType === 'raid' + ? profile.completedRaidPhases + : profile.completedDungeonParts + const sectionName = activity.contentType === 'raid' ? 'Phase' : 'Part' + const parts = [ + { part: 1, name: `${sectionName} 1`, encounterCount: 3, unlocked: true }, + { part: 2, name: `${sectionName} 2`, encounterCount: 3, unlocked: completedSections >= 1 }, + { part: 3, name: `${sectionName} 3`, encounterCount: 3, unlocked: completedSections >= 2 }, + ] + const lootChanceSlots = activity.contentType === 'raid' ? 8 : 5 + const lootPreviewEncounters = [...activity.encounters] + .filter((encounter) => encounter.isBoss) + .sort((a, b) => lootSort === 'boss' + ? a.enemyName.localeCompare(b.enemyName) || a.sequence - b.sequence + : a.sequence - b.sequence) + + return ( +
+
+ +
+ {profile.character.name} + Level {profile.character.level} + Item Level {profile.gearStats.averageItemLevel.toFixed(1)} +
+ +
+ +
+
+ + {screen === 'menu' && ( +
+
+ {MENU_ITEMS.map((item) => ( + + ))} +
+
+ )} + + {screen === 'roguelike' && ( +
+ setScreen('menu')} + /> +
+ + +
+ {roguelikeVariant === 'pve' && ( + <> +
+
+

Upgrade Timing

+

Buff Drafts

+
+
+ + +
+
+
+
+

Upgrade Labels

+

Display Mode

+
+
+ + +
+
+
+ + +
+ + )} + {roguelikeVariant === 'pvp' && ( + <> +
+
+

Match Type

+

PvP Roguelike

+
+
+ + +
+
+
+ {gameMode === 'offline' ? 'C' : 'Q'} +
+ {gameMode === 'offline' ? 'Offline CPU Match' : 'Queue Then CPU Fallback'} + + {gameMode === 'offline' + ? 'Offline mode always places you against a random CPU 1-5.' + : 'Online mode searches briefly. If nobody is queued, a random CPU 1-5 takes the slot.'} + +
+ +
+
+
+
+

CPU Leaderboard

+

{pvpContentType === 'raid' ? 'Raid Clash' : 'Dungeon Clash'}

+
+
+
+
+ Rank + Player + CPU + Clears + Result +
+ {cpuLeaderboard.map((entry, index) => ( +
+ #{index + 1} + {entry.characterName} + CPU {entry.cpuDifficulty} + {entry.encountersCleared} + {entry.result} +
+ ))} + {cpuLeaderboard.length === 0 && ( +
+ No CPU runs recorded yet for this mode. +
+ )} +
+
+ + )} +
+ )} + + {(screen === 'dungeons' || screen === 'raids') && ( +
+ setScreen('menu')} + /> +
+
+ {activityInitials(activity.name)} +
+
+

{activity.locationName}

+

{activity.name}

+

{activity.description}

+
+ Level {activity.recommendedLevel} + {activity.partySize} Players + {selectedDifficulty.name} + Component Level {selectedDifficulty.droppedItemLevel} + {Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP +
+
+ {activityOptions.length > 1 && ( + + )} +
+ {parts.map((p) => ( + + ))} +
+
+
+
+
+

Challenge Tier

+

Difficulty

+
+ +
+
+
+ {selectedDifficulty.name} + {difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : selectedDifficulty.description} +
+
+
Health
{selectedDifficulty.healthMultiplier.toFixed(2)}x
+
Damage
{selectedDifficulty.damageMultiplier.toFixed(2)}x
+
XP
{selectedDifficulty.experienceMultiplier.toFixed(1)}x
+
Components
iLvl {selectedDifficulty.droppedItemLevel}
+
+
+
+
+
+
+

Encounter Rewards

+

{selectedDifficulty.name} Loot Tables

+
+ +
+ {showLoot && ( + <> +
+ +
+

+ Bosses roll {lootChanceSlots} loot chances against the listed table, 1-3 materials each + {activity.completionItemLevel + ? ` - full clear guarantees iLvl ${activity.completionItemLevel}` + : ''} +

+
+ {lootPreviewEncounters.map((encounter) => { + const loot = encounter.lootTables.filter( + (entry) => entry.difficultyId === selectedDifficulty.id, + ) + return ( +
+
+ {encounter.isBoss ? ( + {`${encounter.enemyName} + ) : ( + {encounter.sequence} + )} +
+ {encounter.enemyName} + {loot.length > 0 ? `${lootChanceSlots} weighted chances, 1-3 each` : 'No component table'} +
+
+
+ {loot.map((item) => ( +
+ {item.glyph} +
+ {item.name} + {item.slot} - iLvl {item.itemLevel} +
+ {item.dropWeight}% weight +
+ ))} +
+
+ ) + })} +
+ + )} +
+
+
+
+

Efficiency Rankings

+

{selectedDifficulty.name} Leaderboard

+
+ +
+ {showLeaderboard && ( + <> +

+ {gameMode === 'offline' + ? 'Offline runs are not submitted' + : 'Lowest resource spent ranks first'} +

+
+ {([ + { key: 'part_1', label: `${sectionName} 1` }, + { key: 'part_2', label: `${sectionName} 2` }, + { key: 'part_3', label: `${sectionName} 3` }, + { key: 'full_run', label: 'Full Run' }, + ] as const).map((tab) => ( + + ))} +
+
+
+ Rank + Healer + Class + Level + Item Level + Resource + Time +
+ {activity.leaderboards[leaderboardCategory] + .filter((entry) => entry.difficultyId === selectedDifficulty.id) + .map((entry) => ( +
+ #{entry.rank} + {entry.characterName} + {entry.className} + {entry.characterLevel} + {entry.averageItemLevel.toFixed(1)} + {entry.resourceSpent} + {entry.durationSeconds}s +
+ ))} + {activity.leaderboards[leaderboardCategory].filter( + (entry) => entry.difficultyId === selectedDifficulty.id, + ).length === 0 && ( +
+ {gameMode === 'offline' + ? 'Connect with an online character to compete in rankings.' + : 'Complete this difficulty to claim the first ranking.'} +
+ )} +
+ + )} +
+
+ )} + + {screen === 'customize' && ( + setScreen('menu')} + onSaved={setProfile} + /> + )} + + {screen === 'talents' && ( + setScreen('menu')} + onUpdated={setProfile} + /> + )} + + {screen === 'equipment' && ( + setScreen('menu')} + onUpdated={setProfile} + /> + )} + + {screen === 'settings' && ( + setScreen('menu')} /> + )} + +
+ ) +} + +function ScreenHeading({ + eyebrow, + title, + onBack, +}: { + eyebrow: string + title: string + onBack: () => void +}) { + return ( +
+
+

{eyebrow}

+

{title}

+
+ +
+ ) +} + +export default App diff --git a/src/admin.tsx b/src/admin.tsx new file mode 100644 index 0000000..d314694 --- /dev/null +++ b/src/admin.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import './App.css' +import { AdminScreen } from './components/AdminScreen' + +createRoot(document.getElementById('root')!).render( + + window.close()} /> + , +) diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/AdminScreen.tsx b/src/components/AdminScreen.tsx new file mode 100644 index 0000000..da30de1 --- /dev/null +++ b/src/components/AdminScreen.tsx @@ -0,0 +1,766 @@ +import { useEffect, useState } from 'react' + +type AdminItem = { + id: number + slug: string + name: string + slot: string + rarity: string + itemLevel: number + healingPower: number + maxResourceBonus: number + glyph: string + imageUrl: string + description: string +} + +type AdminEncounter = { + id: number + dungeonId: number + sequence: number + slug: string + enemyName: string + encounterType: string + imageUrl: string +} + +type AdminDifficulty = { + id: number + slug: string + name: string + droppedItemLevel: number +} + +type AdminLootEntry = { + encounterId: number + itemId: number + difficultyId: number + dropWeight: number + dropChance: number +} + +type AdminRecipeComponent = { + itemId: number + quantity: number +} + +type AdminRecipe = { + id: number + itemId: number + difficultyId: number | null + sourceDungeonId: number | null + sourceEncounterId: number | null + components: AdminRecipeComponent[] +} + +type AdminDungeon = { + id: number + slug: string + name: string +} + +type AdminData = { + items: AdminItem[] + encounters: AdminEncounter[] + difficulties: AdminDifficulty[] + encounterLoot: AdminLootEntry[] + craftingRecipes: AdminRecipe[] + dungeons: AdminDungeon[] +} + +const API = '/api/admin' + +async function fetchJson(url: string, init?: RequestInit): Promise { + const res = await fetch(url, init) + const body = await res.json() + if (!res.ok) throw new Error(body.error ?? 'Request failed') + return body +} + +export function AdminScreen({ onBack }: { onBack: () => void }) { + const [data, setData] = useState(null) + const [tab, setTab] = useState<'items' | 'bosses' | 'loot' | 'crafting'>('items') + const [error, setError] = useState('') + const [saving, setSaving] = useState>({}) + + useEffect(() => { + fetchJson(`${API}/data`) + .then(setData) + .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load')) + }, []) + + if (error) return

{error}

+ if (!data) return

Loading admin data...

+ + return ( +
+
+

Developer Tools

Admin Panel

+ +
+ + {tab === 'items' && } + {tab === 'bosses' && } + {tab === 'loot' && } + {tab === 'crafting' && } +
+ ) +} + +function ItemsTab({ data, setData, setSaving, saving }: { + data: AdminData | null + setData: React.Dispatch> + setSaving: (s: Record | ((prev: Record) => Record)) => void + saving: Record +}) { + if (!data) return null + const [filter, setFilter] = useState('') + const [editId, setEditId] = useState(null) + const [form, setForm] = useState>({}) + + const groups = groupBy(data.items.filter((i) => + i.name.toLowerCase().includes(filter.toLowerCase()) || i.slug.includes(filter)), + (i) => i.slot, + ) + + async function saveItem(id: number) { + setSaving((prev) => ({ ...prev, [`item-${id}`]: true })) + try { + const body: Record = {} + if (form.name !== undefined) body.name = form.name + if (form.glyph !== undefined) body.glyph = form.glyph + if (form.description !== undefined) body.description = form.description + if (form.rarity !== undefined) body.rarity = form.rarity + if (form.slot !== undefined) body.slot = form.slot + if (form.itemLevel !== undefined) body.item_level = form.itemLevel + if (form.healingPower !== undefined) body.healing_power = form.healingPower + if (form.maxResourceBonus !== undefined) body.max_resource_bonus = form.maxResourceBonus + await fetchJson(`${API}/items/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + setData((prev) => prev ? { + ...prev, + items: prev.items.map((i) => i.id === id ? { ...i, ...form } : i), + } : prev) + setEditId(null) + setForm({}) + } catch (e: unknown) { + alert(e instanceof Error ? e.message : 'Save failed') + } finally { + setSaving((prev) => ({ ...prev, [`item-${id}`]: false })) + } + } + + return ( +
+ setFilter(e.target.value)} /> + {Object.entries(groups).map(([slot, items]) => ( +
+ {slot} ({items.length}) +
+ {items.map((item) => ( +
+ {editId === item.id ? ( +
+ + + + + + + +