Compare commits

...

26 Commits

Author SHA1 Message Date
Warren H 14bec979e6 Android build v1.0.45 2026-06-20 23:13:55 -04:00
Warren H 4b45483ac3 Android build v1.0.44 2026-06-20 23:04:39 -04:00
Warren H 6e10b37f8e Android build v1.0.43 2026-06-20 22:34:38 -04:00
Warren H 5aac39c6c9 Android build v1.0.42 2026-06-20 20:49:46 -04:00
Warren H 224249e372 Android build v1.0.41 2026-06-20 20:42:10 -04:00
Warren H 66f5af4484 Android build v1.0.40 2026-06-20 20:16:20 -04:00
Warren H 8f5a957963 Android build v1.0.39 2026-06-20 20:09:57 -04:00
Warren H f8a1fbc5e2 Android build v1.0.38 2026-06-20 18:06:39 -04:00
Warren H bab2dce6c3 Android build v1.0.37 2026-06-20 17:52:12 -04:00
Warren H cb38042eca Android build v1.0.37 2026-06-20 17:50:57 -04:00
Warren H 753bba581a Android build v1.0.36 2026-06-20 16:57:03 -04:00
Warren H 2300973164 Android build v1.0.35 2026-06-20 16:10:35 -04:00
Warren H 1281be69d8 Android build v1.0.34 2026-06-20 15:08:51 -04:00
Warren H 4fc15ebe9a Android build v1.0.33 2026-06-20 13:21:49 -04:00
Warren H 7313c968e6 Android build v1.0.32 2026-06-20 12:50:48 -04:00
Warren H 207fcd1a15 Android build v1.0.31 2026-06-19 23:14:56 -04:00
Warren H fd6a1ce3c7 Android build v1.0.30 2026-06-19 22:10:14 -04:00
Warren H f8b98e6b23 Android build v1.0.29 2026-06-19 21:58:23 -04:00
Warren H fc7c6488ea Android build v1.0.28 2026-06-19 21:35:17 -04:00
Warren H ba6d3b614e Android build v1.0.27 2026-06-19 21:29:44 -04:00
Warren H 88874933c3 Android build v1.0.26 2026-06-19 20:55:23 -04:00
Warren H bf12aefeeb Update game 1.0.27 2026-06-19 16:00:47 -04:00
Warren H 814eb1998d Android build v1.0.25 2026-06-18 23:28:43 -04:00
Warren H 7fe62d8c82 Android build v1.0.24 2026-06-18 23:21:00 -04:00
Warren H 3a8d5ad8c5 changes 2026-06-18 22:28:04 -04:00
Warren H a604569a2f made some changes to the UI, removed leaderboards. updated gamesaves 2026-06-18 13:00:29 -04:00
58 changed files with 16361 additions and 8671 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "local-plugins",
"interface": {
"displayName": "Local Plugins"
},
"plugins": [
{
"name": "caveman",
"source": {
"source": "local",
"path": "./.codex-plugins/caveman"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
@@ -0,0 +1,39 @@
{
"name": "caveman",
"version": "0.1.0",
"description": "Ultra-compressed communication mode. Cut filler. Keep technical accuracy.",
"author": {
"name": "Julius Brussee",
"url": "https://github.com/JuliusBrussee"
},
"homepage": "https://github.com/JuliusBrussee/caveman",
"repository": "https://github.com/JuliusBrussee/caveman",
"license": "MIT",
"keywords": [
"productivity",
"communication",
"brevity",
"writing"
],
"skills": "./skills/",
"interface": {
"displayName": "Caveman",
"shortDescription": "Talk like caveman. Cut filler. Keep technical accuracy.",
"longDescription": "Ultra-compressed communication mode for Codex. Use fewer words. Keep exact technical substance.",
"developerName": "Julius Brussee",
"category": "Productivity",
"capabilities": [
"Write"
],
"websiteURL": "https://github.com/JuliusBrussee/caveman",
"privacyPolicyURL": "https://github.com/JuliusBrussee/caveman/blob/main/README.md",
"termsOfServiceURL": "https://github.com/JuliusBrussee/caveman/blob/main/LICENSE",
"defaultPrompt": [
"Use caveman mode. Cut filler. Keep technical accuracy."
],
"composerIcon": "./assets/caveman-small.svg",
"logo": "./assets/caveman.svg",
"screenshots": [],
"brandColor": "#6B7280"
}
}
@@ -0,0 +1,63 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
+9
View File
@@ -0,0 +1,9 @@
# Project Notes
- AYN Thor main display: 6-inch AMOLED, 1920 x 1080, 120Hz.
- AYN Thor secondary display: 3.92-inch AMOLED, 1240 x 1080, 60Hz.
- AYN Thor UI sizing must be designed against Android CSS/layout viewport, not physical framebuffer pixels.
- Approximate Thor CSS viewports: main display 960 x 540, secondary display 620 x 540.
- Test top-screen UI only against the main display viewport, and bottom-screen UI only against the secondary display viewport.
- User rebuilds app; do not rebuild APK unless explicitly requested.
- Apply game changes to both web version and mobile app version.
+221
View File
@@ -43,6 +43,227 @@ 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.
## TrueNAS single-container hosting
### TrueNAS SCALE runbook
This is the simplest TrueNAS setup. One container serves the browser game,
auth routes, game API routes, and one SQLite database. Use this when you want
`iwanttoheal.phenomrom.com` to host the playable browser version and you want
code updates to be a Git pull plus app restart.
Portainer is not required. Use TrueNAS **Apps > Discover > Install via YAML**.
Repository:
```text
https://git.whoagland.com/phenom/i-want-to-heal.git
```
TrueNAS paths:
```text
/mnt/usbssds/apps/iwanttoheal/app
/mnt/usbssds/apps/iwanttoheal/data
```
Create the app directory and clone the repo:
```sh
sudo mkdir -p /mnt/usbssds/apps/iwanttoheal
cd /mnt/usbssds/apps/iwanttoheal
sudo git clone https://git.whoagland.com/phenom/i-want-to-heal.git app
```
Because the clone was run with `sudo`, give the normal TrueNAS user ownership:
```sh
sudo chown -R truenas_admin:truenas_admin /mnt/usbssds/apps/iwanttoheal
```
Create the persistent data folder:
```sh
mkdir -p /mnt/usbssds/apps/iwanttoheal/data
```
Check that the production server file exists:
```sh
ls /mnt/usbssds/apps/iwanttoheal/app/server/production.mjs
```
If that file is missing, push the latest code to `git.whoagland.com` from the
development machine, then pull on TrueNAS:
```sh
cd /mnt/usbssds/apps/iwanttoheal/app
git pull
```
If Git fails with `chmod ... Operation not permitted`, do not use a media or SMB
dataset for the repo. Git needs normal file locking and chmod behavior. Create or
use a dedicated apps dataset and clone under `/mnt/usbssds/apps/...`.
### TrueNAS app YAML
In TrueNAS:
1. Open **Apps**.
2. Open **Discover**.
3. Click the three-dot menu.
4. Choose **Install via YAML**.
5. Name the app `iwanttoheal`.
6. Paste this YAML:
```yaml
services:
iwanttoheal:
image: node:24-bookworm-slim
working_dir: /app
command: sh -lc "npm ci && npm run db:init && npm run build && npm start"
environment:
HOST: 0.0.0.0
PORT: "4173"
TRUST_PROXY: "1"
COOKIE_SECURE: "1"
CORS_ORIGINS: "http://localhost,https://localhost,capacitor://localhost,https://iwanttoheal.phenomrom.com,https://auth.phenomrom.com"
ports:
- "4173:4173"
volumes:
- /mnt/usbssds/apps/iwanttoheal/app:/app
- /mnt/usbssds/apps/iwanttoheal/data:/app/data
restart: unless-stopped
```
The app listens inside Docker on port `4173`. The database lives at
`/mnt/usbssds/apps/iwanttoheal/data/game.db` because that host directory is
mounted into the container as `/app/data`. This is persistent runtime data, not
code. Do not commit it and do not copy the Mac `data/game.db` over it during
deploys.
The startup command installs dependencies, applies schema/static-content
updates, builds the web app, and starts the production server.
Test the local TrueNAS service:
```sh
curl http://TRUENAS-IP:4173/api/auth/session
```
Expected response:
```json
{"account":null,"profile":null}
```
### Reverse proxy
Point `iwanttoheal.phenomrom.com` at the TrueNAS app through HTTPS. Do not expose
port `4173` directly to the internet. Put Caddy or another reverse proxy in
front:
```caddyfile
iwanttoheal.phenomrom.com {
reverse_proxy TRUENAS-IP:4173
}
auth.phenomrom.com {
reverse_proxy TRUENAS-IP:4173
}
```
Both hostnames can point at the same container. `iwanttoheal.phenomrom.com`
serves the browser game. `auth.phenomrom.com` stays available as an auth URL for
Android or other clients that need a dedicated auth hostname.
DNS should point both hostnames at the public IP or dynamic DNS name that reaches
the reverse proxy. Forward public ports `80` and `443` to the reverse proxy host.
Test the public game and auth URLs:
```sh
curl https://iwanttoheal.phenomrom.com
curl https://auth.phenomrom.com/api/auth/session
```
Expected auth response:
```json
{"account":null,"profile":null}
```
### App build config
For the hosted browser game, no separate auth build setting is needed. The web
app can call same-origin routes like `/api/auth/login` and `/api/profile`.
For an Android build that should use the TrueNAS-hosted game API, build with:
```sh
npm run android:apk:truenas
```
If you intentionally want Android auth calls to use `auth.phenomrom.com`, also
set `VITE_AUTH_API_BASE_URL=https://auth.phenomrom.com`. Otherwise, leave it
unset and auth uses the same base URL as the game API.
Android runs the bundled web app from a local Capacitor origin, not from
`iwanttoheal.phenomrom.com`. The hosted server must allow that origin through
CORS, which is why the TrueNAS YAML includes `http://localhost`,
`https://localhost`, and `capacitor://localhost`.
### Updating the TrueNAS game app
Push changes from the development machine to `git.whoagland.com`, then pull them
on TrueNAS:
```sh
cd /mnt/usbssds/apps/iwanttoheal/app
git pull
```
Before restarting, back up the persistent database:
```sh
cp /mnt/usbssds/apps/iwanttoheal/data/game.db \
"/mnt/usbssds/apps/iwanttoheal/data/game-before-update-$(date +%Y%m%d-%H%M%S).db"
```
Restart the `iwanttoheal` app in the TrueNAS Apps UI after pulling. The app
command runs `npm ci`, `npm run db:init`, `npm run build`, and `npm start` on
startup, so dependency, schema, and browser bundle changes are applied each time
the container restarts.
`npm run db:init` updates schema and seeded static game content. It should not
erase accounts, characters, inventory, or save progress. Character resets are
separate manual operations and should only be run intentionally.
Normal update workflow:
```sh
# development machine
git add .
git commit -m "Update game"
git push origin main
# TrueNAS shell
cd /mnt/usbssds/apps/iwanttoheal/app
git pull
cp /mnt/usbssds/apps/iwanttoheal/data/game.db \
"/mnt/usbssds/apps/iwanttoheal/data/game-before-update-$(date +%Y%m%d-%H%M%S).db"
```
Then restart the TrueNAS app.
For the shorter checklist, see [docs/push-updates.md](docs/push-updates.md).
### Existing auth-only app
If `iwanttoheal-auth` was already created during earlier testing, the simplest
path is to stop that app and use the single `iwanttoheal` app above. The single
container serves both domains and avoids two processes sharing one SQLite file.
## Account limits
Registration permits one account per public IP by default. Login and API rate
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -6
View File
@@ -57,13 +57,13 @@ For an online production build, see [DEPLOYMENT.md](DEPLOYMENT.md).
- Dungeon XP rewards, level-up handling, talent-point awards, and ability unlocks
- SQLite-backed class talent trees with ranks, tiers, prerequisites, refunds,
and immediate persistence
- Seven equipment slots, starter item-level 1 gear, inventory comparison, and
persistent equipping
- Nine equipment slots, empty starter inventory, craftable item-level 1 gear,
inventory comparison, and persistent equipping
- Aggregate item level, healing power, and resource bonuses that affect combat
- Five SQLite-authored dungeon difficulty tiers with level gates, combat
scaling, XP multipliers, and item-level reward bands
- Encounter-specific weighted loot tables for every difficulty, with authored
drop chances, slot pools, and item-level 5 through 25 reward variants
- Four playable SQLite-authored content tiers at item levels 1, 10, 20, and 25
with level gates, combat scaling, XP multipliers, and reward bands
- Gear progression through item levels 1, 5, 10, 15, 20, and 25 with
boss-coin crafting and upgrade steps
- One live loot roll per defeated encounter, shown in the combat log and
dungeon-complete summary
- Atomic inventory awards with retry-safe roll records and stacked duplicate
+14 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 65
versionName "1.0.45"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -24,6 +24,18 @@ android {
}
}
def invalidAndroidResCopies = tasks.register('removeInvalidAndroidResCopies', Delete) {
delete fileTree("${projectDir}/src/main/res") {
include '**/* *.*'
}
}
tasks.matching { task ->
task.name.startsWith('merge') && task.name.endsWith('Resources')
}.configureEach {
dependsOn(invalidAndroidResCopies)
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
@@ -2,17 +2,25 @@ package com.warren.iwanttoheal;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.KeyEvent;
import android.view.View;
import com.getcapacitor.BridgeActivity;
import java.io.File;
public abstract class ControllerBridgeActivity extends BridgeActivity {
public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL";
private static final long DPAD_THROTTLE_MS = 220;
private long lastDpadDispatchAt = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
clearWebViewServiceWorkers();
super.onCreate(savedInstanceState);
if (bridge != null) {
bridge.getWebView().clearCache(true);
}
loadIntentUrl();
}
@@ -47,6 +55,25 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
);
}
private void clearWebViewServiceWorkers() {
File webViewData = new File(getApplicationInfo().dataDir, "app_webview");
deleteIfExists(new File(webViewData, "Default/Service Worker"));
deleteIfExists(new File(webViewData, "Service Worker"));
}
private void deleteIfExists(File file) {
if (!file.exists()) return;
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteIfExists(child);
}
}
}
file.delete();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
String token = controllerToken(event.getKeyCode());
@@ -54,6 +81,7 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
boolean repeat = event.getRepeatCount() > 0;
if (isDpadToken(token) && shouldThrottleDpad()) return true;
String script =
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
@@ -64,6 +92,20 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
return true;
}
private boolean shouldThrottleDpad() {
long now = SystemClock.uptimeMillis();
if (now - lastDpadDispatchAt < DPAD_THROTTLE_MS) return true;
lastDpadDispatchAt = now;
return false;
}
private boolean isDpadToken(String token) {
return token.equals("Button12")
|| token.equals("Button13")
|| token.equals("Button14")
|| token.equals("Button15");
}
private String controllerToken(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A:
@@ -66,9 +66,10 @@ public class DualScreenPlugin extends Plugin {
}
String gameUrl = bridge.getLocalUrl();
String topGameUrl = gameUrl + "/?display=top";
String controlsUrl = gameUrl + "/?display=bottom";
String targetUrl = launchPlan.targetIsTopDisplay ? gameUrl : controlsUrl;
String currentUrl = launchPlan.currentIsTopDisplay ? gameUrl : controlsUrl;
String targetUrl = launchPlan.targetIsTopDisplay ? topGameUrl : controlsUrl;
String currentUrl = launchPlan.currentIsTopDisplay ? topGameUrl : controlsUrl;
closePresentation();
presentation = new TopDisplayPresentation(
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+638
View File
@@ -0,0 +1,638 @@
-- Generated by local admin panel. Commit this file with uploaded art changes.
PRAGMA foreign_keys = ON;
BEGIN TRANSACTION;
UPDATE dungeons SET slug = 'bulldrome-hunting-ground', name = 'Bulldrome Hunting Ground', recommended_level = 1, content_type = 'dungeon', party_size = 6, experience_reward = 125, image_url = '/api/dungeon-images/bulldrome-hunting-ground-1782008229745-0b8d91e2.png', description = 'A focused hunt through Bulldrome territory.' WHERE id = 1;
UPDATE dungeons SET slug = 'yian-kut-ku-hunting-ground', name = 'Yian Kut-Ku Hunting Ground', recommended_level = 1, content_type = 'dungeon', party_size = 6, experience_reward = 125, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Yian Kut-Ku territory.' WHERE id = 2;
UPDATE dungeons SET slug = 'rathian-hunting-ground', name = 'Rathian Hunting Ground', recommended_level = 1, content_type = 'dungeon', party_size = 6, experience_reward = 125, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Rathian territory.' WHERE id = 3;
UPDATE dungeons SET slug = 'tigrex-hunting-ground', name = 'Tigrex Hunting Ground', recommended_level = 10, content_type = 'dungeon', party_size = 6, experience_reward = 205, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Tigrex territory.' WHERE id = 4;
UPDATE dungeons SET slug = 'rathalos-hunting-ground', name = 'Rathalos Hunting Ground', recommended_level = 10, content_type = 'dungeon', party_size = 6, experience_reward = 205, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Rathalos territory.' WHERE id = 5;
UPDATE dungeons SET slug = 'gypceros-hunting-ground', name = 'Gypceros Hunting Ground', recommended_level = 10, content_type = 'dungeon', party_size = 6, experience_reward = 205, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Gypceros territory.' WHERE id = 6;
UPDATE dungeons SET slug = 'nargacuga-hunting-ground', name = 'Nargacuga Hunting Ground', recommended_level = 15, content_type = 'dungeon', party_size = 6, experience_reward = 245, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Nargacuga territory.' WHERE id = 7;
UPDATE dungeons SET slug = 'azuros-hunting-ground', name = 'Azuros Hunting Ground', recommended_level = 15, content_type = 'dungeon', party_size = 6, experience_reward = 245, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Azuros territory.' WHERE id = 8;
UPDATE dungeons SET slug = 'diablos-hunting-ground', name = 'Diablos Hunting Ground', recommended_level = 15, content_type = 'dungeon', party_size = 6, experience_reward = 245, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Diablos territory.' WHERE id = 9;
UPDATE dungeons SET slug = 'barroth-hunting-ground', name = 'Barroth Hunting Ground', recommended_level = 20, content_type = 'dungeon', party_size = 6, experience_reward = 285, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Barroth territory.' WHERE id = 10;
UPDATE dungeons SET slug = 'tobi-kadachi-hunting-ground', name = 'Tobi Kadachi Hunting Ground', recommended_level = 20, content_type = 'dungeon', party_size = 6, experience_reward = 285, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Tobi Kadachi territory.' WHERE id = 11;
UPDATE dungeons SET slug = 'monoblos-hunting-ground', name = 'Monoblos Hunting Ground', recommended_level = 20, content_type = 'dungeon', party_size = 6, experience_reward = 285, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Monoblos territory.' WHERE id = 12;
UPDATE dungeons SET slug = 'anjanath-hunting-ground', name = 'Anjanath Hunting Ground', recommended_level = 25, content_type = 'dungeon', party_size = 6, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Anjanath territory.' WHERE id = 13;
UPDATE dungeons SET slug = 'bazelgeuse-hunting-ground', name = 'Bazelgeuse Hunting Ground', recommended_level = 25, content_type = 'dungeon', party_size = 6, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Bazelgeuse territory.' WHERE id = 14;
UPDATE dungeons SET slug = 'odogaron-hunting-ground', name = 'Odogaron Hunting Ground', recommended_level = 25, content_type = 'dungeon', party_size = 6, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A focused hunt through Odogaron territory.' WHERE id = 15;
UPDATE dungeons SET slug = 'apex-tigrex-raid', name = 'Apex Tigrex Raid', recommended_level = 10, content_type = 'raid', party_size = 18, experience_reward = 275, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Tigrex.' WHERE id = 20;
UPDATE dungeons SET slug = 'apex-rathalos-raid', name = 'Apex Rathalos Raid', recommended_level = 10, content_type = 'raid', party_size = 18, experience_reward = 275, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Rathalos.' WHERE id = 21;
UPDATE dungeons SET slug = 'apex-gypceros-raid', name = 'Apex Gypceros Raid', recommended_level = 10, content_type = 'raid', party_size = 18, experience_reward = 275, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Gypceros.' WHERE id = 22;
UPDATE dungeons SET slug = 'apex-nargacuga-raid', name = 'Apex Nargacuga Raid', recommended_level = 15, content_type = 'raid', party_size = 18, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Nargacuga.' WHERE id = 23;
UPDATE dungeons SET slug = 'apex-azuros-raid', name = 'Apex Azuros Raid', recommended_level = 15, content_type = 'raid', party_size = 18, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Azuros.' WHERE id = 24;
UPDATE dungeons SET slug = 'apex-diablos-raid', name = 'Apex Diablos Raid', recommended_level = 15, content_type = 'raid', party_size = 18, experience_reward = 325, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Diablos.' WHERE id = 25;
UPDATE dungeons SET slug = 'apex-barroth-raid', name = 'Apex Barroth Raid', recommended_level = 20, content_type = 'raid', party_size = 18, experience_reward = 375, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Barroth.' WHERE id = 26;
UPDATE dungeons SET slug = 'apex-tobi-kadachi-raid', name = 'Apex Tobi Kadachi Raid', recommended_level = 20, content_type = 'raid', party_size = 18, experience_reward = 375, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Tobi Kadachi.' WHERE id = 27;
UPDATE dungeons SET slug = 'apex-monoblos-raid', name = 'Apex Monoblos Raid', recommended_level = 20, content_type = 'raid', party_size = 18, experience_reward = 375, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Monoblos.' WHERE id = 28;
UPDATE dungeons SET slug = 'apex-anjanath-raid', name = 'Apex Anjanath Raid', recommended_level = 25, content_type = 'raid', party_size = 18, experience_reward = 425, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Anjanath.' WHERE id = 29;
UPDATE dungeons SET slug = 'apex-bazelgeuse-raid', name = 'Apex Bazelgeuse Raid', recommended_level = 25, content_type = 'raid', party_size = 18, experience_reward = 425, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Bazelgeuse.' WHERE id = 30;
UPDATE dungeons SET slug = 'apex-odogaron-raid', name = 'Apex Odogaron Raid', recommended_level = 25, content_type = 'raid', party_size = 18, experience_reward = 425, image_url = '/boss-placeholder.svg', description = 'A raid-scale hunt against Apex Odogaron.' WHERE id = 31;
UPDATE encounters SET slug = 'bulldrome-approach', name = 'Bulldrome Approach', encounter_type = 'trash', max_health = 465, base_damage = 14, tank_damage = 9, party_damage = 26, description = 'Hunters clear the path before Bulldrome.', image_url = '/api/boss-images/bulldrome-approach-1782009080839-e94761e7.png' WHERE id = 101;
UPDATE encounters SET slug = 'bulldrome-guardians', name = 'Bulldrome Guardians', encounter_type = 'trash', max_health = 555, base_damage = 16, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Bulldrome.', image_url = '/api/boss-images/bulldrome-guardians-1782009078446-a2d2e266.png' WHERE id = 102;
UPDATE encounters SET slug = 'bulldrome-boss', name = 'Bulldrome', encounter_type = 'boss', max_health = 895, base_damage = 20, tank_damage = 15, party_damage = 31, description = 'Bulldrome drops boss coins for crafting.', image_url = '/api/boss-images/bulldrome-boss-1782009066079-e670cb7e.png' WHERE id = 103;
UPDATE encounters SET slug = 'yian-kut-ku-approach', name = 'Yian Kut-Ku Approach', encounter_type = 'trash', max_health = 465, base_damage = 14, tank_damage = 9, party_damage = 26, description = 'Hunters clear the path before Yian Kut-Ku.', image_url = '/boss-placeholder.svg' WHERE id = 201;
UPDATE encounters SET slug = 'yian-kut-ku-guardians', name = 'Yian Kut-Ku Guardians', encounter_type = 'trash', max_health = 555, base_damage = 16, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Yian Kut-Ku.', image_url = '/boss-placeholder.svg' WHERE id = 202;
UPDATE encounters SET slug = 'yian-kut-ku-boss', name = 'Yian Kut-Ku', encounter_type = 'boss', max_health = 895, base_damage = 20, tank_damage = 15, party_damage = 31, description = 'Yian Kut-Ku drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 203;
UPDATE encounters SET slug = 'rathian-approach', name = 'Rathian Approach', encounter_type = 'trash', max_health = 465, base_damage = 14, tank_damage = 9, party_damage = 26, description = 'Hunters clear the path before Rathian.', image_url = '/boss-placeholder.svg' WHERE id = 301;
UPDATE encounters SET slug = 'rathian-guardians', name = 'Rathian Guardians', encounter_type = 'trash', max_health = 555, base_damage = 16, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Rathian.', image_url = '/boss-placeholder.svg' WHERE id = 302;
UPDATE encounters SET slug = 'rathian-boss', name = 'Rathian', encounter_type = 'boss', max_health = 895, base_damage = 20, tank_damage = 15, party_damage = 31, description = 'Rathian drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 303;
UPDATE encounters SET slug = 'tigrex-approach', name = 'Tigrex Approach', encounter_type = 'trash', max_health = 780, base_damage = 15, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Tigrex.', image_url = '/boss-placeholder.svg' WHERE id = 401;
UPDATE encounters SET slug = 'tigrex-guardians', name = 'Tigrex Guardians', encounter_type = 'trash', max_health = 870, base_damage = 17, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Tigrex.', image_url = '/boss-placeholder.svg' WHERE id = 402;
UPDATE encounters SET slug = 'tigrex-boss', name = 'Tigrex', encounter_type = 'boss', max_health = 1210, base_damage = 21, tank_damage = 16, party_damage = 33, description = 'Tigrex drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 403;
UPDATE encounters SET slug = 'rathalos-approach', name = 'Rathalos Approach', encounter_type = 'trash', max_health = 780, base_damage = 15, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Rathalos.', image_url = '/boss-placeholder.svg' WHERE id = 501;
UPDATE encounters SET slug = 'rathalos-guardians', name = 'Rathalos Guardians', encounter_type = 'trash', max_health = 870, base_damage = 17, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Rathalos.', image_url = '/boss-placeholder.svg' WHERE id = 502;
UPDATE encounters SET slug = 'rathalos-boss', name = 'Rathalos', encounter_type = 'boss', max_health = 1210, base_damage = 21, tank_damage = 16, party_damage = 33, description = 'Rathalos drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 503;
UPDATE encounters SET slug = 'gypceros-approach', name = 'Gypceros Approach', encounter_type = 'trash', max_health = 780, base_damage = 15, tank_damage = 10, party_damage = 28, description = 'Hunters clear the path before Gypceros.', image_url = '/boss-placeholder.svg' WHERE id = 601;
UPDATE encounters SET slug = 'gypceros-guardians', name = 'Gypceros Guardians', encounter_type = 'trash', max_health = 870, base_damage = 17, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Gypceros.', image_url = '/boss-placeholder.svg' WHERE id = 602;
UPDATE encounters SET slug = 'gypceros-boss', name = 'Gypceros', encounter_type = 'boss', max_health = 1210, base_damage = 21, tank_damage = 16, party_damage = 33, description = 'Gypceros drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 603;
UPDATE encounters SET slug = 'nargacuga-approach', name = 'Nargacuga Approach', encounter_type = 'trash', max_health = 955, base_damage = 16, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Nargacuga.', image_url = '/boss-placeholder.svg' WHERE id = 701;
UPDATE encounters SET slug = 'nargacuga-guardians', name = 'Nargacuga Guardians', encounter_type = 'trash', max_health = 1045, base_damage = 18, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Nargacuga.', image_url = '/boss-placeholder.svg' WHERE id = 702;
UPDATE encounters SET slug = 'nargacuga-boss', name = 'Nargacuga', encounter_type = 'boss', max_health = 1385, base_damage = 22, tank_damage = 17, party_damage = 35, description = 'Nargacuga drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 703;
UPDATE encounters SET slug = 'azuros-approach', name = 'Azuros Approach', encounter_type = 'trash', max_health = 955, base_damage = 16, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Azuros.', image_url = '/boss-placeholder.svg' WHERE id = 801;
UPDATE encounters SET slug = 'azuros-guardians', name = 'Azuros Guardians', encounter_type = 'trash', max_health = 1045, base_damage = 18, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Azuros.', image_url = '/boss-placeholder.svg' WHERE id = 802;
UPDATE encounters SET slug = 'azuros-boss', name = 'Azuros', encounter_type = 'boss', max_health = 1385, base_damage = 22, tank_damage = 17, party_damage = 35, description = 'Azuros drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 803;
UPDATE encounters SET slug = 'diablos-approach', name = 'Diablos Approach', encounter_type = 'trash', max_health = 955, base_damage = 16, tank_damage = 11, party_damage = 30, description = 'Hunters clear the path before Diablos.', image_url = '/boss-placeholder.svg' WHERE id = 901;
UPDATE encounters SET slug = 'diablos-guardians', name = 'Diablos Guardians', encounter_type = 'trash', max_health = 1045, base_damage = 18, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Diablos.', image_url = '/boss-placeholder.svg' WHERE id = 902;
UPDATE encounters SET slug = 'diablos-boss', name = 'Diablos', encounter_type = 'boss', max_health = 1385, base_damage = 22, tank_damage = 17, party_damage = 35, description = 'Diablos drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 903;
UPDATE encounters SET slug = 'barroth-approach', name = 'Barroth Approach', encounter_type = 'trash', max_health = 1130, base_damage = 17, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Barroth.', image_url = '/boss-placeholder.svg' WHERE id = 1001;
UPDATE encounters SET slug = 'barroth-guardians', name = 'Barroth Guardians', encounter_type = 'trash', max_health = 1220, base_damage = 19, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Barroth.', image_url = '/boss-placeholder.svg' WHERE id = 1002;
UPDATE encounters SET slug = 'barroth-boss', name = 'Barroth', encounter_type = 'boss', max_health = 1560, base_damage = 23, tank_damage = 18, party_damage = 37, description = 'Barroth drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1003;
UPDATE encounters SET slug = 'tobi-kadachi-approach', name = 'Tobi Kadachi Approach', encounter_type = 'trash', max_health = 1130, base_damage = 17, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Tobi Kadachi.', image_url = '/boss-placeholder.svg' WHERE id = 1101;
UPDATE encounters SET slug = 'tobi-kadachi-guardians', name = 'Tobi Kadachi Guardians', encounter_type = 'trash', max_health = 1220, base_damage = 19, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Tobi Kadachi.', image_url = '/boss-placeholder.svg' WHERE id = 1102;
UPDATE encounters SET slug = 'tobi-kadachi-boss', name = 'Tobi Kadachi', encounter_type = 'boss', max_health = 1560, base_damage = 23, tank_damage = 18, party_damage = 37, description = 'Tobi Kadachi drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1103;
UPDATE encounters SET slug = 'monoblos-approach', name = 'Monoblos Approach', encounter_type = 'trash', max_health = 1130, base_damage = 17, tank_damage = 12, party_damage = 32, description = 'Hunters clear the path before Monoblos.', image_url = '/boss-placeholder.svg' WHERE id = 1201;
UPDATE encounters SET slug = 'monoblos-guardians', name = 'Monoblos Guardians', encounter_type = 'trash', max_health = 1220, base_damage = 19, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Monoblos.', image_url = '/boss-placeholder.svg' WHERE id = 1202;
UPDATE encounters SET slug = 'monoblos-boss', name = 'Monoblos', encounter_type = 'boss', max_health = 1560, base_damage = 23, tank_damage = 18, party_damage = 37, description = 'Monoblos drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1203;
UPDATE encounters SET slug = 'anjanath-approach', name = 'Anjanath Approach', encounter_type = 'trash', max_health = 1305, base_damage = 18, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Anjanath.', image_url = '/boss-placeholder.svg' WHERE id = 1301;
UPDATE encounters SET slug = 'anjanath-guardians', name = 'Anjanath Guardians', encounter_type = 'trash', max_health = 1395, base_damage = 20, tank_damage = 14, party_damage = 36, description = 'Hunters clear the path before Anjanath.', image_url = '/boss-placeholder.svg' WHERE id = 1302;
UPDATE encounters SET slug = 'anjanath-boss', name = 'Anjanath', encounter_type = 'boss', max_health = 1735, base_damage = 24, tank_damage = 19, party_damage = 39, description = 'Anjanath drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1303;
UPDATE encounters SET slug = 'bazelgeuse-approach', name = 'Bazelgeuse Approach', encounter_type = 'trash', max_health = 1305, base_damage = 18, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Bazelgeuse.', image_url = '/boss-placeholder.svg' WHERE id = 1401;
UPDATE encounters SET slug = 'bazelgeuse-guardians', name = 'Bazelgeuse Guardians', encounter_type = 'trash', max_health = 1395, base_damage = 20, tank_damage = 14, party_damage = 36, description = 'Hunters clear the path before Bazelgeuse.', image_url = '/boss-placeholder.svg' WHERE id = 1402;
UPDATE encounters SET slug = 'bazelgeuse-boss', name = 'Bazelgeuse', encounter_type = 'boss', max_health = 1735, base_damage = 24, tank_damage = 19, party_damage = 39, description = 'Bazelgeuse drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1403;
UPDATE encounters SET slug = 'odogaron-approach', name = 'Odogaron Approach', encounter_type = 'trash', max_health = 1305, base_damage = 18, tank_damage = 13, party_damage = 34, description = 'Hunters clear the path before Odogaron.', image_url = '/boss-placeholder.svg' WHERE id = 1501;
UPDATE encounters SET slug = 'odogaron-guardians', name = 'Odogaron Guardians', encounter_type = 'trash', max_health = 1395, base_damage = 20, tank_damage = 14, party_damage = 36, description = 'Hunters clear the path before Odogaron.', image_url = '/boss-placeholder.svg' WHERE id = 1502;
UPDATE encounters SET slug = 'odogaron-boss', name = 'Odogaron', encounter_type = 'boss', max_health = 1735, base_damage = 24, tank_damage = 19, party_damage = 39, description = 'Odogaron drops boss coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 1503;
UPDATE encounters SET slug = 'tigrex-raid-approach', name = 'Apex Tigrex Approach', encounter_type = 'trash', max_health = 1900, base_damage = 17, tank_damage = 11, party_damage = 54, description = 'Hunters clear the raid path before Apex Tigrex.', image_url = '/boss-placeholder.svg' WHERE id = 2001;
UPDATE encounters SET slug = 'tigrex-raid-guardians', name = 'Apex Tigrex Guardians', encounter_type = 'trash', max_health = 1970, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Tigrex.', image_url = '/boss-placeholder.svg' WHERE id = 2002;
UPDATE encounters SET slug = 'tigrex-raid-boss', name = 'Apex Tigrex', encounter_type = 'boss', max_health = 2230, base_damage = 22, tank_damage = 16, party_damage = 60, description = 'Apex Tigrex drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2003;
UPDATE encounters SET slug = 'rathalos-raid-approach', name = 'Apex Rathalos Approach', encounter_type = 'trash', max_health = 1900, base_damage = 17, tank_damage = 11, party_damage = 54, description = 'Hunters clear the raid path before Apex Rathalos.', image_url = '/boss-placeholder.svg' WHERE id = 2101;
UPDATE encounters SET slug = 'rathalos-raid-guardians', name = 'Apex Rathalos Guardians', encounter_type = 'trash', max_health = 1970, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Rathalos.', image_url = '/boss-placeholder.svg' WHERE id = 2102;
UPDATE encounters SET slug = 'rathalos-raid-boss', name = 'Apex Rathalos', encounter_type = 'boss', max_health = 2230, base_damage = 22, tank_damage = 16, party_damage = 60, description = 'Apex Rathalos drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2103;
UPDATE encounters SET slug = 'gypceros-raid-approach', name = 'Apex Gypceros Approach', encounter_type = 'trash', max_health = 1900, base_damage = 17, tank_damage = 11, party_damage = 54, description = 'Hunters clear the raid path before Apex Gypceros.', image_url = '/boss-placeholder.svg' WHERE id = 2201;
UPDATE encounters SET slug = 'gypceros-raid-guardians', name = 'Apex Gypceros Guardians', encounter_type = 'trash', max_health = 1970, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Gypceros.', image_url = '/boss-placeholder.svg' WHERE id = 2202;
UPDATE encounters SET slug = 'gypceros-raid-boss', name = 'Apex Gypceros', encounter_type = 'boss', max_health = 2230, base_damage = 22, tank_damage = 16, party_damage = 60, description = 'Apex Gypceros drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2203;
UPDATE encounters SET slug = 'nargacuga-raid-approach', name = 'Apex Nargacuga Approach', encounter_type = 'trash', max_health = 2075, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Nargacuga.', image_url = '/boss-placeholder.svg' WHERE id = 2301;
UPDATE encounters SET slug = 'nargacuga-raid-guardians', name = 'Apex Nargacuga Guardians', encounter_type = 'trash', max_health = 2145, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Nargacuga.', image_url = '/boss-placeholder.svg' WHERE id = 2302;
UPDATE encounters SET slug = 'nargacuga-raid-boss', name = 'Apex Nargacuga', encounter_type = 'boss', max_health = 2405, base_damage = 23, tank_damage = 17, party_damage = 62, description = 'Apex Nargacuga drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2303;
UPDATE encounters SET slug = 'azuros-raid-approach', name = 'Apex Azuros Approach', encounter_type = 'trash', max_health = 2075, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Azuros.', image_url = '/boss-placeholder.svg' WHERE id = 2401;
UPDATE encounters SET slug = 'azuros-raid-guardians', name = 'Apex Azuros Guardians', encounter_type = 'trash', max_health = 2145, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Azuros.', image_url = '/boss-placeholder.svg' WHERE id = 2402;
UPDATE encounters SET slug = 'azuros-raid-boss', name = 'Apex Azuros', encounter_type = 'boss', max_health = 2405, base_damage = 23, tank_damage = 17, party_damage = 62, description = 'Apex Azuros drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2403;
UPDATE encounters SET slug = 'diablos-raid-approach', name = 'Apex Diablos Approach', encounter_type = 'trash', max_health = 2075, base_damage = 18, tank_damage = 12, party_damage = 56, description = 'Hunters clear the raid path before Apex Diablos.', image_url = '/boss-placeholder.svg' WHERE id = 2501;
UPDATE encounters SET slug = 'diablos-raid-guardians', name = 'Apex Diablos Guardians', encounter_type = 'trash', max_health = 2145, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Diablos.', image_url = '/boss-placeholder.svg' WHERE id = 2502;
UPDATE encounters SET slug = 'diablos-raid-boss', name = 'Apex Diablos', encounter_type = 'boss', max_health = 2405, base_damage = 23, tank_damage = 17, party_damage = 62, description = 'Apex Diablos drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2503;
UPDATE encounters SET slug = 'barroth-raid-approach', name = 'Apex Barroth Approach', encounter_type = 'trash', max_health = 2250, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Barroth.', image_url = '/boss-placeholder.svg' WHERE id = 2601;
UPDATE encounters SET slug = 'barroth-raid-guardians', name = 'Apex Barroth Guardians', encounter_type = 'trash', max_health = 2320, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Barroth.', image_url = '/boss-placeholder.svg' WHERE id = 2602;
UPDATE encounters SET slug = 'barroth-raid-boss', name = 'Apex Barroth', encounter_type = 'boss', max_health = 2580, base_damage = 24, tank_damage = 18, party_damage = 64, description = 'Apex Barroth drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2603;
UPDATE encounters SET slug = 'tobi-kadachi-raid-approach', name = 'Apex Tobi Kadachi Approach', encounter_type = 'trash', max_health = 2250, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Tobi Kadachi.', image_url = '/boss-placeholder.svg' WHERE id = 2701;
UPDATE encounters SET slug = 'tobi-kadachi-raid-guardians', name = 'Apex Tobi Kadachi Guardians', encounter_type = 'trash', max_health = 2320, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Tobi Kadachi.', image_url = '/boss-placeholder.svg' WHERE id = 2702;
UPDATE encounters SET slug = 'tobi-kadachi-raid-boss', name = 'Apex Tobi Kadachi', encounter_type = 'boss', max_health = 2580, base_damage = 24, tank_damage = 18, party_damage = 64, description = 'Apex Tobi Kadachi drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2703;
UPDATE encounters SET slug = 'monoblos-raid-approach', name = 'Apex Monoblos Approach', encounter_type = 'trash', max_health = 2250, base_damage = 19, tank_damage = 13, party_damage = 58, description = 'Hunters clear the raid path before Apex Monoblos.', image_url = '/boss-placeholder.svg' WHERE id = 2801;
UPDATE encounters SET slug = 'monoblos-raid-guardians', name = 'Apex Monoblos Guardians', encounter_type = 'trash', max_health = 2320, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Monoblos.', image_url = '/boss-placeholder.svg' WHERE id = 2802;
UPDATE encounters SET slug = 'monoblos-raid-boss', name = 'Apex Monoblos', encounter_type = 'boss', max_health = 2580, base_damage = 24, tank_damage = 18, party_damage = 64, description = 'Apex Monoblos drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2803;
UPDATE encounters SET slug = 'anjanath-raid-approach', name = 'Apex Anjanath Approach', encounter_type = 'trash', max_health = 2425, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Anjanath.', image_url = '/boss-placeholder.svg' WHERE id = 2901;
UPDATE encounters SET slug = 'anjanath-raid-guardians', name = 'Apex Anjanath Guardians', encounter_type = 'trash', max_health = 2495, base_damage = 21, tank_damage = 15, party_damage = 62, description = 'Hunters clear the raid path before Apex Anjanath.', image_url = '/boss-placeholder.svg' WHERE id = 2902;
UPDATE encounters SET slug = 'anjanath-raid-boss', name = 'Apex Anjanath', encounter_type = 'boss', max_health = 2755, base_damage = 25, tank_damage = 19, party_damage = 66, description = 'Apex Anjanath drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 2903;
UPDATE encounters SET slug = 'bazelgeuse-raid-approach', name = 'Apex Bazelgeuse Approach', encounter_type = 'trash', max_health = 2425, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Bazelgeuse.', image_url = '/boss-placeholder.svg' WHERE id = 3001;
UPDATE encounters SET slug = 'bazelgeuse-raid-guardians', name = 'Apex Bazelgeuse Guardians', encounter_type = 'trash', max_health = 2495, base_damage = 21, tank_damage = 15, party_damage = 62, description = 'Hunters clear the raid path before Apex Bazelgeuse.', image_url = '/boss-placeholder.svg' WHERE id = 3002;
UPDATE encounters SET slug = 'bazelgeuse-raid-boss', name = 'Apex Bazelgeuse', encounter_type = 'boss', max_health = 2755, base_damage = 25, tank_damage = 19, party_damage = 66, description = 'Apex Bazelgeuse drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 3003;
UPDATE encounters SET slug = 'odogaron-raid-approach', name = 'Apex Odogaron Approach', encounter_type = 'trash', max_health = 2425, base_damage = 20, tank_damage = 14, party_damage = 60, description = 'Hunters clear the raid path before Apex Odogaron.', image_url = '/boss-placeholder.svg' WHERE id = 3101;
UPDATE encounters SET slug = 'odogaron-raid-guardians', name = 'Apex Odogaron Guardians', encounter_type = 'trash', max_health = 2495, base_damage = 21, tank_damage = 15, party_damage = 62, description = 'Hunters clear the raid path before Apex Odogaron.', image_url = '/boss-placeholder.svg' WHERE id = 3102;
UPDATE encounters SET slug = 'odogaron-raid-boss', name = 'Apex Odogaron', encounter_type = 'boss', max_health = 2755, base_damage = 25, tank_damage = 19, party_damage = 66, description = 'Apex Odogaron drops raid coins for crafting.', image_url = '/boss-placeholder.svg' WHERE id = 3103;
UPDATE items SET slug = 'emberglass-sigil', name = 'Honed Yian Kut-Ku Ring', slot = 'ring', rarity = 'uncommon', item_level = 5, healing_power = 4, max_resource_bonus = 5, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 1;
UPDATE items SET slug = 'wardens-cinderwrap', name = 'Honed Bulldrome Chest', slot = 'chest', rarity = 'uncommon', item_level = 5, healing_power = 3, max_resource_bonus = 0, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bulldrome coins.' WHERE id = 2;
UPDATE items SET slug = 'ashwood-crook', name = 'Honed Rathian Weapon', slot = 'weapon', rarity = 'uncommon', item_level = 5, healing_power = 5, max_resource_bonus = 0, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 3;
UPDATE items SET slug = 'cinderstep-boots', name = 'Honed Yian Kut-Ku Boots', slot = 'boots', rarity = 'uncommon', item_level = 5, healing_power = 3, max_resource_bonus = 0, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 4;
UPDATE items SET slug = 'adepts-hood', name = 'Honed Bulldrome Helmet', slot = 'helmet', rarity = 'uncommon', item_level = 5, healing_power = 3, max_resource_bonus = 4, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bulldrome coins.' WHERE id = 5;
UPDATE items SET slug = 'furnace-tenders-wraps', name = 'Honed Bulldrome Gloves', slot = 'gloves', rarity = 'uncommon', item_level = 5, healing_power = 3, max_resource_bonus = 2, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bulldrome coins.' WHERE id = 6;
UPDATE items SET slug = 'warden-ember', name = 'Honed Yian Kut-Ku Trinket', slot = 'trinket', rarity = 'uncommon', item_level = 5, healing_power = 4, max_resource_bonus = 4, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 7;
UPDATE items SET slug = 'ashwalker-legwraps', name = 'Honed Rathian Pants', slot = 'pants', rarity = 'uncommon', item_level = 5, healing_power = 3, max_resource_bonus = 3, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 8;
UPDATE items SET slug = 'sootglass-pendant', name = 'Honed Rathian Necklace', slot = 'necklace', rarity = 'uncommon', item_level = 5, healing_power = 4, max_resource_bonus = 4, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 9;
UPDATE items SET slug = 'novice-crook', name = 'Raw Rathian Weapon', slot = 'weapon', rarity = 'common', item_level = 1, healing_power = 1, max_resource_bonus = 0, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 100;
UPDATE items SET slug = 'novice-cowl', name = 'Raw Legacy Loot Encounter 3 Helmet', slot = 'helmet', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 1, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Legacy Loot Encounter 3 coins.' WHERE id = 101;
UPDATE items SET slug = 'novice-vestment', name = 'Raw Legacy Loot Encounter 3 Chest', slot = 'chest', rarity = 'common', item_level = 1, healing_power = 1, max_resource_bonus = 0, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Legacy Loot Encounter 3 coins.' WHERE id = 102;
UPDATE items SET slug = 'novice-wraps', name = 'Raw Legacy Loot Encounter 3 Gloves', slot = 'gloves', rarity = 'common', item_level = 1, healing_power = 1, max_resource_bonus = 0, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Legacy Loot Encounter 3 coins.' WHERE id = 103;
UPDATE items SET slug = 'novice-slippers', name = 'Raw Yian Kut-Ku Boots', slot = 'boots', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 1, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 104;
UPDATE items SET slug = 'novice-band', name = 'Raw Yian Kut-Ku Ring', slot = 'ring', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 1, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 105;
UPDATE items SET slug = 'novice-token', name = 'Raw Yian Kut-Ku Trinket', slot = 'trinket', rarity = 'common', item_level = 1, healing_power = 1, max_resource_bonus = 0, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Yian Kut-Ku coins.' WHERE id = 106;
UPDATE items SET slug = 'novice-wand', name = 'Novice Wand', slot = 'weapon', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 2, glyph = '!', image_url = '/equipment-placeholder.svg', description = 'A spare focus that favors a deeper resource pool.' WHERE id = 107;
UPDATE items SET slug = 'novice-trousers', name = 'Raw Rathian Pants', slot = 'pants', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 1, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 108;
UPDATE items SET slug = 'novice-pendant', name = 'Raw Rathian Necklace', slot = 'necklace', rarity = 'common', item_level = 1, healing_power = 1, max_resource_bonus = 0, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathian coins.' WHERE id = 109;
UPDATE items SET slug = 'tempered-emberglass-sigil', name = 'Green Rathalos Ring', slot = 'ring', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 9, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathalos coins.' WHERE id = 201;
UPDATE items SET slug = 'tempered-cinderwrap', name = 'Green Tigrex Chest', slot = 'chest', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 2, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tigrex coins.' WHERE id = 202;
UPDATE items SET slug = 'tempered-ashwood-crook', name = 'Green Gypceros Weapon', slot = 'weapon', rarity = 'uncommon', item_level = 10, healing_power = 10, max_resource_bonus = 2, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Gypceros coins.' WHERE id = 203;
UPDATE items SET slug = 'tempered-cinderstep-boots', name = 'Green Rathalos Boots', slot = 'boots', rarity = 'uncommon', item_level = 10, healing_power = 6, max_resource_bonus = 5, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathalos coins.' WHERE id = 204;
UPDATE items SET slug = 'tempered-adepts-hood', name = 'Green Tigrex Helmet', slot = 'helmet', rarity = 'uncommon', item_level = 10, healing_power = 6, max_resource_bonus = 6, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tigrex coins.' WHERE id = 205;
UPDATE items SET slug = 'tempered-furnace-wraps', name = 'Green Tigrex Gloves', slot = 'gloves', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 4, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tigrex coins.' WHERE id = 206;
UPDATE items SET slug = 'tempered-warden-ember', name = 'Green Rathalos Trinket', slot = 'trinket', rarity = 'uncommon', item_level = 10, healing_power = 8, max_resource_bonus = 7, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Rathalos coins.' WHERE id = 207;
UPDATE items SET slug = 'tempered-ashwalker-legwraps', name = 'Green Gypceros Pants', slot = 'pants', rarity = 'uncommon', item_level = 10, healing_power = 6, max_resource_bonus = 6, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Gypceros coins.' WHERE id = 208;
UPDATE items SET slug = 'tempered-sootglass-pendant', name = 'Green Gypceros Necklace', slot = 'necklace', rarity = 'uncommon', item_level = 10, healing_power = 8, max_resource_bonus = 7, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Gypceros coins.' WHERE id = 209;
UPDATE items SET slug = 'runed-emberglass-sigil', name = 'Blue Azuros Ring', slot = 'ring', rarity = 'rare', item_level = 15, healing_power = 10, max_resource_bonus = 13, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Azuros coins.' WHERE id = 301;
UPDATE items SET slug = 'runed-cinderwrap', name = 'Blue Nargacuga Chest', slot = 'chest', rarity = 'rare', item_level = 15, healing_power = 11, max_resource_bonus = 3, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Nargacuga coins.' WHERE id = 302;
UPDATE items SET slug = 'runed-ashwood-crook', name = 'Blue Diablos Weapon', slot = 'weapon', rarity = 'rare', item_level = 15, healing_power = 15, max_resource_bonus = 3, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Diablos coins.' WHERE id = 303;
UPDATE items SET slug = 'runed-cinderstep-boots', name = 'Blue Azuros Boots', slot = 'boots', rarity = 'rare', item_level = 15, healing_power = 9, max_resource_bonus = 8, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Azuros coins.' WHERE id = 304;
UPDATE items SET slug = 'runed-adepts-hood', name = 'Blue Nargacuga Helmet', slot = 'helmet', rarity = 'rare', item_level = 15, healing_power = 9, max_resource_bonus = 9, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Nargacuga coins.' WHERE id = 305;
UPDATE items SET slug = 'runed-furnace-wraps', name = 'Blue Nargacuga Gloves', slot = 'gloves', rarity = 'rare', item_level = 15, healing_power = 11, max_resource_bonus = 6, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Nargacuga coins.' WHERE id = 306;
UPDATE items SET slug = 'runed-warden-ember', name = 'Blue Azuros Trinket', slot = 'trinket', rarity = 'rare', item_level = 15, healing_power = 12, max_resource_bonus = 10, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Azuros coins.' WHERE id = 307;
UPDATE items SET slug = 'runed-ashwalker-legwraps', name = 'Blue Diablos Pants', slot = 'pants', rarity = 'rare', item_level = 15, healing_power = 9, max_resource_bonus = 9, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Diablos coins.' WHERE id = 308;
UPDATE items SET slug = 'runed-sootglass-pendant', name = 'Blue Diablos Necklace', slot = 'necklace', rarity = 'rare', item_level = 15, healing_power = 12, max_resource_bonus = 10, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Diablos coins.' WHERE id = 309;
UPDATE items SET slug = 'mythic-emberglass-sigil', name = 'Purple Tobi Kadachi Ring', slot = 'ring', rarity = 'epic', item_level = 20, healing_power = 14, max_resource_bonus = 17, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tobi Kadachi coins.' WHERE id = 401;
UPDATE items SET slug = 'mythic-cinderwrap', name = 'Purple Barroth Chest', slot = 'chest', rarity = 'epic', item_level = 20, healing_power = 15, max_resource_bonus = 4, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Barroth coins.' WHERE id = 402;
UPDATE items SET slug = 'mythic-ashwood-crook', name = 'Purple Monoblos Weapon', slot = 'weapon', rarity = 'epic', item_level = 20, healing_power = 20, max_resource_bonus = 4, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Monoblos coins.' WHERE id = 403;
UPDATE items SET slug = 'mythic-cinderstep-boots', name = 'Purple Tobi Kadachi Boots', slot = 'boots', rarity = 'epic', item_level = 20, healing_power = 12, max_resource_bonus = 11, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tobi Kadachi coins.' WHERE id = 404;
UPDATE items SET slug = 'mythic-adepts-hood', name = 'Purple Barroth Helmet', slot = 'helmet', rarity = 'epic', item_level = 20, healing_power = 12, max_resource_bonus = 12, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Barroth coins.' WHERE id = 405;
UPDATE items SET slug = 'mythic-furnace-wraps', name = 'Purple Barroth Gloves', slot = 'gloves', rarity = 'epic', item_level = 20, healing_power = 15, max_resource_bonus = 8, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Barroth coins.' WHERE id = 406;
UPDATE items SET slug = 'mythic-warden-ember', name = 'Purple Tobi Kadachi Trinket', slot = 'trinket', rarity = 'epic', item_level = 20, healing_power = 16, max_resource_bonus = 13, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Tobi Kadachi coins.' WHERE id = 407;
UPDATE items SET slug = 'mythic-ashwalker-legwraps', name = 'Purple Monoblos Pants', slot = 'pants', rarity = 'epic', item_level = 20, healing_power = 12, max_resource_bonus = 12, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Monoblos coins.' WHERE id = 408;
UPDATE items SET slug = 'mythic-sootglass-pendant', name = 'Purple Monoblos Necklace', slot = 'necklace', rarity = 'epic', item_level = 20, healing_power = 16, max_resource_bonus = 13, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Monoblos coins.' WHERE id = 409;
UPDATE items SET slug = 'ascendant-emberglass-sigil', name = 'Orange Bazelgeuse Ring', slot = 'ring', rarity = 'legendary', item_level = 25, healing_power = 18, max_resource_bonus = 21, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bazelgeuse coins.' WHERE id = 501;
UPDATE items SET slug = 'ascendant-cinderwrap', name = 'Orange Anjanath Chest', slot = 'chest', rarity = 'legendary', item_level = 25, healing_power = 19, max_resource_bonus = 5, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Anjanath coins.' WHERE id = 502;
UPDATE items SET slug = 'ascendant-ashwood-crook', name = 'Orange Odogaron Weapon', slot = 'weapon', rarity = 'legendary', item_level = 25, healing_power = 25, max_resource_bonus = 5, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Odogaron coins.' WHERE id = 503;
UPDATE items SET slug = 'ascendant-cinderstep-boots', name = 'Orange Bazelgeuse Boots', slot = 'boots', rarity = 'legendary', item_level = 25, healing_power = 15, max_resource_bonus = 14, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bazelgeuse coins.' WHERE id = 504;
UPDATE items SET slug = 'ascendant-adepts-hood', name = 'Orange Anjanath Helmet', slot = 'helmet', rarity = 'legendary', item_level = 25, healing_power = 15, max_resource_bonus = 15, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Anjanath coins.' WHERE id = 505;
UPDATE items SET slug = 'ascendant-furnace-wraps', name = 'Orange Anjanath Gloves', slot = 'gloves', rarity = 'legendary', item_level = 25, healing_power = 19, max_resource_bonus = 10, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Anjanath coins.' WHERE id = 506;
UPDATE items SET slug = 'ascendant-warden-ember', name = 'Orange Bazelgeuse Trinket', slot = 'trinket', rarity = 'legendary', item_level = 25, healing_power = 20, max_resource_bonus = 16, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Bazelgeuse coins.' WHERE id = 507;
UPDATE items SET slug = 'ascendant-ashwalker-legwraps', name = 'Orange Odogaron Pants', slot = 'pants', rarity = 'legendary', item_level = 25, healing_power = 15, max_resource_bonus = 15, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Odogaron coins.' WHERE id = 508;
UPDATE items SET slug = 'ascendant-sootglass-pendant', name = 'Orange Odogaron Necklace', slot = 'necklace', rarity = 'legendary', item_level = 25, healing_power = 20, max_resource_bonus = 16, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Odogaron coins.' WHERE id = 509;
UPDATE items SET slug = 'minor-component', name = 'Minor Component', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'A basic crafting component.' WHERE id = 600;
UPDATE items SET slug = 'basic-component', name = 'Basic Component', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'A standard crafting component.' WHERE id = 601;
UPDATE items SET slug = 'refined-component', name = 'Refined Component', slot = 'component', rarity = 'common', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'A refined crafting component.' WHERE id = 602;
UPDATE items SET slug = 'advanced-component', name = 'Advanced Component', slot = 'component', rarity = 'common', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'An advanced crafting component.' WHERE id = 603;
UPDATE items SET slug = 'superior-component', name = 'Superior Component', slot = 'component', rarity = 'common', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'A superior crafting component.' WHERE id = 604;
UPDATE items SET slug = 'primal-component', name = 'Primal Component', slot = 'component', rarity = 'common', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '', image_url = '/equipment-placeholder.svg', description = 'A primal crafting component.' WHERE id = 605;
UPDATE items SET slug = 'caldera-signet', name = 'Caldera Signet', slot = 'ring', rarity = 'rare', item_level = 7, healing_power = 5, max_resource_bonus = 6, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'A raid-forged signet warm with caldera light.' WHERE id = 701;
UPDATE items SET slug = 'vanguard-mantle', name = 'Vanguard Mantle', slot = 'chest', rarity = 'rare', item_level = 7, healing_power = 5, max_resource_bonus = 1, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'A reinforced mantle taken from the citadel vanguard.' WHERE id = 702;
UPDATE items SET slug = 'pyrebinder-crook', name = 'Pyrebinder Crook', slot = 'weapon', rarity = 'rare', item_level = 7, healing_power = 7, max_resource_bonus = 1, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'A focus used to bend living flame toward restoration.' WHERE id = 703;
UPDATE items SET slug = 'emberstep-treads', name = 'Emberstep Treads', slot = 'boots', rarity = 'rare', item_level = 7, healing_power = 4, max_resource_bonus = 5, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Raid treads made for crossing unstable volcanic stone.' WHERE id = 704;
UPDATE items SET slug = 'gatekeeper-cowl', name = 'Gatekeeper Cowl', slot = 'helmet', rarity = 'rare', item_level = 7, healing_power = 4, max_resource_bonus = 6, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'A cowl inscribed with the wards of the outer gate.' WHERE id = 705;
UPDATE items SET slug = 'crownward-wraps', name = 'Crownward Wraps', slot = 'gloves', rarity = 'rare', item_level = 7, healing_power = 5, max_resource_bonus = 3, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Precise wraps worn by the healers of the Ember Crown.' WHERE id = 706;
UPDATE items SET slug = 'living-coal-charm', name = 'Living Coal Charm', slot = 'trinket', rarity = 'rare', item_level = 7, healing_power = 6, max_resource_bonus = 5, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'A coal that pulses in time with nearby heartbeats.' WHERE id = 707;
UPDATE items SET slug = 'caldera-legwraps', name = 'Caldera Legwraps', slot = 'pants', rarity = 'rare', item_level = 7, healing_power = 4, max_resource_bonus = 6, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Raid legwraps made for unstable volcanic stone.' WHERE id = 708;
UPDATE items SET slug = 'gateflame-pendant', name = 'Gateflame Pendant', slot = 'necklace', rarity = 'rare', item_level = 7, healing_power = 6, max_resource_bonus = 5, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'A pendant bearing a cooled mote of gateflame.' WHERE id = 709;
UPDATE items SET slug = 'royal-caldera-signet', name = 'Green Apex Rathalos Ring', slot = 'ring', rarity = 'uncommon', item_level = 10, healing_power = 8, max_resource_bonus = 9, glyph = 'o', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Rathalos coins.' WHERE id = 710;
UPDATE items SET slug = 'ember-crown-vestment', name = 'Green Apex Tigrex Chest', slot = 'chest', rarity = 'uncommon', item_level = 10, healing_power = 8, max_resource_bonus = 2, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Tigrex coins.' WHERE id = 711;
UPDATE items SET slug = 'crownshard-crook', name = 'Green Apex Gypceros Weapon', slot = 'weapon', rarity = 'uncommon', item_level = 10, healing_power = 11, max_resource_bonus = 2, glyph = '/', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Gypceros coins.' WHERE id = 712;
UPDATE items SET slug = 'caldera-walkers', name = 'Green Apex Rathalos Boots', slot = 'boots', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 8, glyph = 'b', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Rathalos coins.' WHERE id = 713;
UPDATE items SET slug = 'inquisitors-cowl', name = 'Green Apex Tigrex Helmet', slot = 'helmet', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 9, glyph = '^', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Tigrex coins.' WHERE id = 714;
UPDATE items SET slug = 'royal-flame-wraps', name = 'Green Apex Tigrex Gloves', slot = 'gloves', rarity = 'uncommon', item_level = 10, healing_power = 8, max_resource_bonus = 6, glyph = 'g', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Tigrex coins.' WHERE id = 715;
UPDATE items SET slug = 'extinguished-crown', name = 'Green Apex Rathalos Trinket', slot = 'trinket', rarity = 'uncommon', item_level = 10, healing_power = 9, max_resource_bonus = 8, glyph = '*', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Rathalos coins.' WHERE id = 716;
UPDATE items SET slug = 'royal-caldera-legwraps', name = 'Green Apex Gypceros Pants', slot = 'pants', rarity = 'uncommon', item_level = 10, healing_power = 7, max_resource_bonus = 9, glyph = 'P', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Gypceros coins.' WHERE id = 717;
UPDATE items SET slug = 'ember-crown-pendant', name = 'Green Apex Gypceros Necklace', slot = 'necklace', rarity = 'uncommon', item_level = 10, healing_power = 9, max_resource_bonus = 8, glyph = 'n', image_url = '/equipment-placeholder.svg', description = 'Crafted with Apex Gypceros coins.' WHERE id = 718;
UPDATE items SET slug = 'ashen-cowl-pattern', name = 'Ashen Cowl Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'H', image_url = '/equipment-placeholder.svg', description = 'A Warden Vhal pattern used to craft helmets.' WHERE id = 800;
UPDATE items SET slug = 'ashen-vestment-pattern', name = 'Ashen Vestment Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'A Warden Vhal pattern used to craft chest pieces.' WHERE id = 801;
UPDATE items SET slug = 'ashen-wrap-pattern', name = 'Ashen Wrap Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'G', image_url = '/equipment-placeholder.svg', description = 'A Warden Vhal pattern used to craft gloves.' WHERE id = 802;
UPDATE items SET slug = 'cinderstep-pattern', name = 'Cinderstep Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'B', image_url = '/equipment-placeholder.svg', description = 'A Forge-Priestess Haela pattern used to craft boots.' WHERE id = 803;
UPDATE items SET slug = 'emberglass-setting', name = 'Emberglass Setting', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'R', image_url = '/equipment-placeholder.svg', description = 'A Forge-Priestess Haela setting used to craft rings.' WHERE id = 804;
UPDATE items SET slug = 'warden-ember-core', name = 'Warden Ember Core', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'T', image_url = '/equipment-placeholder.svg', description = 'A Forge-Priestess Haela core used to craft trinkets.' WHERE id = 805;
UPDATE items SET slug = 'ashwood-focus-pattern', name = 'Ashwood Focus Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'W', image_url = '/equipment-placeholder.svg', description = 'An Old Furnace pattern used to craft weapons.' WHERE id = 806;
UPDATE items SET slug = 'furnace-legwrap-pattern', name = 'Furnace Legwrap Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'L', image_url = '/equipment-placeholder.svg', description = 'An Old Furnace pattern used to craft pants.' WHERE id = 807;
UPDATE items SET slug = 'sootglass-pendant-pattern', name = 'Sootglass Pendant Pattern', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'N', image_url = '/equipment-placeholder.svg', description = 'An Old Furnace pattern used to craft necklaces.' WHERE id = 808;
UPDATE items SET slug = 'vhal-emberplate', name = 'Vhal Emberplate', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'V', image_url = '/equipment-placeholder.svg', description = 'A boss material from Warden Vhal.' WHERE id = 820;
UPDATE items SET slug = 'haela-forgebrand', name = 'Haela Forgebrand', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'F', image_url = '/equipment-placeholder.svg', description = 'A boss material from Forge-Priestess Haela.' WHERE id = 821;
UPDATE items SET slug = 'old-furnace-heartshard', name = 'Old Furnace Heartshard', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = 'O', image_url = '/equipment-placeholder.svg', description = 'A boss material from the Old Furnace.' WHERE id = 822;
UPDATE items SET slug = 'gatekeeper-cowl-pattern', name = 'Gatekeeper Cowl Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'H', image_url = '/equipment-placeholder.svg', description = 'A Gatekeeper Arkon pattern used to craft raid helmets.' WHERE id = 830;
UPDATE items SET slug = 'crownward-vestment-pattern', name = 'Crownward Vestment Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'C', image_url = '/equipment-placeholder.svg', description = 'A Gatekeeper Arkon pattern used to craft raid chest pieces.' WHERE id = 831;
UPDATE items SET slug = 'crownward-wrap-pattern', name = 'Crownward Wrap Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'G', image_url = '/equipment-placeholder.svg', description = 'A Gatekeeper Arkon pattern used to craft raid gloves.' WHERE id = 832;
UPDATE items SET slug = 'caldera-tread-pattern', name = 'Caldera Tread Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'B', image_url = '/equipment-placeholder.svg', description = 'A High Inquisitor Vael pattern used to craft raid boots.' WHERE id = 833;
UPDATE items SET slug = 'royal-signet-setting', name = 'Royal Signet Setting', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'R', image_url = '/equipment-placeholder.svg', description = 'A High Inquisitor Vael setting used to craft raid rings.' WHERE id = 834;
UPDATE items SET slug = 'living-coal-vessel', name = 'Living Coal Vessel', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'T', image_url = '/equipment-placeholder.svg', description = 'A High Inquisitor Vael vessel used to craft raid trinkets.' WHERE id = 835;
UPDATE items SET slug = 'crownshard-focus-pattern', name = 'Crownshard Focus Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'W', image_url = '/equipment-placeholder.svg', description = 'An Ember Crown pattern used to craft raid weapons.' WHERE id = 836;
UPDATE items SET slug = 'inquisitor-legwrap-pattern', name = 'Inquisitor Legwrap Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'L', image_url = '/equipment-placeholder.svg', description = 'An Ember Crown pattern used to craft raid pants.' WHERE id = 837;
UPDATE items SET slug = 'crown-pendant-pattern', name = 'Crown Pendant Pattern', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'N', image_url = '/equipment-placeholder.svg', description = 'An Ember Crown pattern used to craft raid necklaces.' WHERE id = 838;
UPDATE items SET slug = 'arkon-gate-sigil', name = 'Arkon Gate Sigil', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'A', image_url = '/equipment-placeholder.svg', description = 'A raid boss material from Gatekeeper Arkon.' WHERE id = 850;
UPDATE items SET slug = 'vael-brandseal', name = 'Vael Brandseal', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'I', image_url = '/equipment-placeholder.svg', description = 'A raid boss material from High Inquisitor Vael.' WHERE id = 851;
UPDATE items SET slug = 'ember-crown-shard', name = 'Ember Crown Shard', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = 'E', image_url = '/equipment-placeholder.svg', description = 'A raid boss material from the Ember Crown.' WHERE id = 852;
UPDATE items SET slug = 'bulldrome-drop-1', name = 'Bulldrome Drop 1', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bulldrome.' WHERE id = 860;
UPDATE items SET slug = 'bulldrome-drop-2', name = 'Bulldrome Drop 2', slot = 'component', rarity = 'rare', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bulldrome.' WHERE id = 861;
UPDATE items SET slug = 'bulldrome-drop-3', name = 'Bulldrome Drop 3', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bulldrome.' WHERE id = 862;
UPDATE items SET slug = 'bulldrome-drop-4', name = 'Bulldrome Drop 4', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bulldrome.' WHERE id = 863;
UPDATE items SET slug = 'yian-kut-ku-drop-1', name = 'Yian Kut-Ku Drop 1', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Yian Kut-Ku.' WHERE id = 864;
UPDATE items SET slug = 'yian-kut-ku-drop-2', name = 'Yian Kut-Ku Drop 2', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Yian Kut-Ku.' WHERE id = 865;
UPDATE items SET slug = 'yian-kut-ku-drop-3', name = 'Yian Kut-Ku Drop 3', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Yian Kut-Ku.' WHERE id = 866;
UPDATE items SET slug = 'yian-kut-ku-drop-4', name = 'Yian Kut-Ku Drop 4', slot = 'component', rarity = 'rare', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Yian Kut-Ku.' WHERE id = 867;
UPDATE items SET slug = 'rathian-drop-1', name = 'Rathian Drop 1', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathian.' WHERE id = 868;
UPDATE items SET slug = 'rathian-drop-2', name = 'Rathian Drop 2', slot = 'component', rarity = 'uncommon', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathian.' WHERE id = 869;
UPDATE items SET slug = 'rathian-drop-3', name = 'Rathian Drop 3', slot = 'component', rarity = 'rare', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathian.' WHERE id = 870;
UPDATE items SET slug = 'rathian-drop-4', name = 'Rathian Drop 4', slot = 'component', rarity = 'epic', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathian.' WHERE id = 871;
UPDATE items SET slug = 'tigrex-drop-1-ilvl-10', name = 'Tigrex Drop 1', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tigrex.' WHERE id = 3101;
UPDATE items SET slug = 'tigrex-drop-2-ilvl-10', name = 'Tigrex Drop 2', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tigrex.' WHERE id = 3102;
UPDATE items SET slug = 'tigrex-drop-3-ilvl-10', name = 'Tigrex Drop 3', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tigrex.' WHERE id = 3103;
UPDATE items SET slug = 'tigrex-drop-4-ilvl-10', name = 'Tigrex Drop 4', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tigrex.' WHERE id = 3104;
UPDATE items SET slug = 'rathalos-drop-1-ilvl-10', name = 'Rathalos Drop 1', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathalos.' WHERE id = 3111;
UPDATE items SET slug = 'rathalos-drop-2-ilvl-10', name = 'Rathalos Drop 2', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathalos.' WHERE id = 3112;
UPDATE items SET slug = 'rathalos-drop-3-ilvl-10', name = 'Rathalos Drop 3', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathalos.' WHERE id = 3113;
UPDATE items SET slug = 'rathalos-drop-4-ilvl-10', name = 'Rathalos Drop 4', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Rathalos.' WHERE id = 3114;
UPDATE items SET slug = 'gypceros-drop-1-ilvl-10', name = 'Gypceros Drop 1', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Gypceros.' WHERE id = 3121;
UPDATE items SET slug = 'gypceros-drop-2-ilvl-10', name = 'Gypceros Drop 2', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Gypceros.' WHERE id = 3122;
UPDATE items SET slug = 'gypceros-drop-3-ilvl-10', name = 'Gypceros Drop 3', slot = 'component', rarity = 'rare', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Gypceros.' WHERE id = 3123;
UPDATE items SET slug = 'gypceros-drop-4-ilvl-10', name = 'Gypceros Drop 4', slot = 'component', rarity = 'epic', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Gypceros.' WHERE id = 3124;
UPDATE items SET slug = 'nargacuga-drop-1-ilvl-15', name = 'Nargacuga Drop 1', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Nargacuga.' WHERE id = 3181;
UPDATE items SET slug = 'nargacuga-drop-2-ilvl-15', name = 'Nargacuga Drop 2', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Nargacuga.' WHERE id = 3182;
UPDATE items SET slug = 'nargacuga-drop-3-ilvl-15', name = 'Nargacuga Drop 3', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Nargacuga.' WHERE id = 3183;
UPDATE items SET slug = 'nargacuga-drop-4-ilvl-15', name = 'Nargacuga Drop 4', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Nargacuga.' WHERE id = 3184;
UPDATE items SET slug = 'azuros-drop-1-ilvl-15', name = 'Azuros Drop 1', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Azuros.' WHERE id = 3191;
UPDATE items SET slug = 'azuros-drop-2-ilvl-15', name = 'Azuros Drop 2', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Azuros.' WHERE id = 3192;
UPDATE items SET slug = 'azuros-drop-3-ilvl-15', name = 'Azuros Drop 3', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Azuros.' WHERE id = 3193;
UPDATE items SET slug = 'azuros-drop-4-ilvl-15', name = 'Azuros Drop 4', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Azuros.' WHERE id = 3194;
UPDATE items SET slug = 'diablos-drop-1-ilvl-15', name = 'Diablos Drop 1', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Diablos.' WHERE id = 3201;
UPDATE items SET slug = 'diablos-drop-2-ilvl-15', name = 'Diablos Drop 2', slot = 'component', rarity = 'uncommon', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Diablos.' WHERE id = 3202;
UPDATE items SET slug = 'diablos-drop-3-ilvl-15', name = 'Diablos Drop 3', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Diablos.' WHERE id = 3203;
UPDATE items SET slug = 'diablos-drop-4-ilvl-15', name = 'Diablos Drop 4', slot = 'component', rarity = 'epic', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Diablos.' WHERE id = 3204;
UPDATE items SET slug = 'barroth-drop-1-ilvl-20', name = 'Barroth Drop 1', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Barroth.' WHERE id = 3261;
UPDATE items SET slug = 'barroth-drop-2-ilvl-20', name = 'Barroth Drop 2', slot = 'component', rarity = 'rare', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Barroth.' WHERE id = 3262;
UPDATE items SET slug = 'barroth-drop-3-ilvl-20', name = 'Barroth Drop 3', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Barroth.' WHERE id = 3263;
UPDATE items SET slug = 'barroth-drop-4-ilvl-20', name = 'Barroth Drop 4', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Barroth.' WHERE id = 3264;
UPDATE items SET slug = 'tobi-kadachi-drop-1-ilvl-20', name = 'Tobi Kadachi Drop 1', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tobi Kadachi.' WHERE id = 3271;
UPDATE items SET slug = 'tobi-kadachi-drop-2-ilvl-20', name = 'Tobi Kadachi Drop 2', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tobi Kadachi.' WHERE id = 3272;
UPDATE items SET slug = 'tobi-kadachi-drop-3-ilvl-20', name = 'Tobi Kadachi Drop 3', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tobi Kadachi.' WHERE id = 3273;
UPDATE items SET slug = 'tobi-kadachi-drop-4-ilvl-20', name = 'Tobi Kadachi Drop 4', slot = 'component', rarity = 'rare', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Tobi Kadachi.' WHERE id = 3274;
UPDATE items SET slug = 'monoblos-drop-1-ilvl-20', name = 'Monoblos Drop 1', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Monoblos.' WHERE id = 3281;
UPDATE items SET slug = 'monoblos-drop-2-ilvl-20', name = 'Monoblos Drop 2', slot = 'component', rarity = 'uncommon', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Monoblos.' WHERE id = 3282;
UPDATE items SET slug = 'monoblos-drop-3-ilvl-20', name = 'Monoblos Drop 3', slot = 'component', rarity = 'rare', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Monoblos.' WHERE id = 3283;
UPDATE items SET slug = 'monoblos-drop-4-ilvl-20', name = 'Monoblos Drop 4', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Monoblos.' WHERE id = 3284;
UPDATE items SET slug = 'anjanath-drop-1-ilvl-25', name = 'Anjanath Drop 1', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Anjanath.' WHERE id = 3341;
UPDATE items SET slug = 'anjanath-drop-2-ilvl-25', name = 'Anjanath Drop 2', slot = 'component', rarity = 'rare', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Anjanath.' WHERE id = 3342;
UPDATE items SET slug = 'anjanath-drop-3-ilvl-25', name = 'Anjanath Drop 3', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Anjanath.' WHERE id = 3343;
UPDATE items SET slug = 'anjanath-drop-4-ilvl-25', name = 'Anjanath Drop 4', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Anjanath.' WHERE id = 3344;
UPDATE items SET slug = 'bazelgeuse-drop-1-ilvl-25', name = 'Bazelgeuse Drop 1', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bazelgeuse.' WHERE id = 3351;
UPDATE items SET slug = 'bazelgeuse-drop-2-ilvl-25', name = 'Bazelgeuse Drop 2', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bazelgeuse.' WHERE id = 3352;
UPDATE items SET slug = 'bazelgeuse-drop-3-ilvl-25', name = 'Bazelgeuse Drop 3', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bazelgeuse.' WHERE id = 3353;
UPDATE items SET slug = 'bazelgeuse-drop-4-ilvl-25', name = 'Bazelgeuse Drop 4', slot = 'component', rarity = 'rare', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Bazelgeuse.' WHERE id = 3354;
UPDATE items SET slug = 'odogaron-drop-1-ilvl-25', name = 'Odogaron Drop 1', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '1', image_url = '/equipment-placeholder.svg', description = 'A monster part from Odogaron.' WHERE id = 3361;
UPDATE items SET slug = 'odogaron-drop-2-ilvl-25', name = 'Odogaron Drop 2', slot = 'component', rarity = 'uncommon', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '2', image_url = '/equipment-placeholder.svg', description = 'A monster part from Odogaron.' WHERE id = 3362;
UPDATE items SET slug = 'odogaron-drop-3-ilvl-25', name = 'Odogaron Drop 3', slot = 'component', rarity = 'rare', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '3', image_url = '/equipment-placeholder.svg', description = 'A monster part from Odogaron.' WHERE id = 3363;
UPDATE items SET slug = 'odogaron-drop-4-ilvl-25', name = 'Odogaron Drop 4', slot = 'component', rarity = 'epic', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '4', image_url = '/equipment-placeholder.svg', description = 'A monster part from Odogaron.' WHERE id = 3364;
UPDATE items SET slug = 'bulldrome-coin-ilvl-1', name = 'Raw Bulldrome Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 1 crafting.' WHERE id = 280301;
UPDATE items SET slug = 'bulldrome-coin-ilvl-5', name = 'Bulldrome Coin', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 5 crafting.' WHERE id = 280305;
UPDATE items SET slug = 'bulldrome-coin-ilvl-10', name = 'Green Bulldrome Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 10 crafting.' WHERE id = 280310;
UPDATE items SET slug = 'bulldrome-coin-ilvl-15', name = 'Blue Bulldrome Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 15 crafting.' WHERE id = 280315;
UPDATE items SET slug = 'bulldrome-coin-ilvl-20', name = 'Purple Bulldrome Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 20 crafting.' WHERE id = 280320;
UPDATE items SET slug = 'bulldrome-coin-ilvl-25', name = 'Orange Bulldrome Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 25 crafting.' WHERE id = 280325;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-1', name = 'Raw Yian Kut-Ku Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 1 crafting.' WHERE id = 281201;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-5', name = 'Yian Kut-Ku Coin', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 5 crafting.' WHERE id = 281205;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-10', name = 'Green Yian Kut-Ku Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 10 crafting.' WHERE id = 281210;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-15', name = 'Blue Yian Kut-Ku Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 15 crafting.' WHERE id = 281215;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-20', name = 'Purple Yian Kut-Ku Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 20 crafting.' WHERE id = 281220;
UPDATE items SET slug = 'yian-kut-ku-coin-ilvl-25', name = 'Orange Yian Kut-Ku Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 25 crafting.' WHERE id = 281225;
UPDATE items SET slug = 'rathian-coin-ilvl-1', name = 'Raw Rathian Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 1 crafting.' WHERE id = 282201;
UPDATE items SET slug = 'rathian-coin-ilvl-5', name = 'Rathian Coin', slot = 'component', rarity = 'common', item_level = 5, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 5 crafting.' WHERE id = 282205;
UPDATE items SET slug = 'rathian-coin-ilvl-10', name = 'Green Rathian Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 10 crafting.' WHERE id = 282210;
UPDATE items SET slug = 'rathian-coin-ilvl-15', name = 'Blue Rathian Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 15 crafting.' WHERE id = 282215;
UPDATE items SET slug = 'rathian-coin-ilvl-20', name = 'Purple Rathian Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 20 crafting.' WHERE id = 282220;
UPDATE items SET slug = 'rathian-coin-ilvl-25', name = 'Orange Rathian Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 25 crafting.' WHERE id = 282225;
UPDATE items SET slug = 'legacy-loot-encounter-3-coin-diff-1-ilvl-1', name = 'Raw Legacy Loot Encounter 3 Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Legacy Loot Encounter 3 used for item level 1 crafting.' WHERE id = 283001;
UPDATE items SET slug = 'legacy-loot-encounter-3-coin-diff-2-ilvl-10', name = 'Green Legacy Loot Encounter 3 Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Legacy Loot Encounter 3 used for item level 10 crafting.' WHERE id = 283002;
UPDATE items SET slug = 'legacy-loot-encounter-3-coin-diff-3-ilvl-15', name = 'Blue Legacy Loot Encounter 3 Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Legacy Loot Encounter 3 used for item level 15 crafting.' WHERE id = 283003;
UPDATE items SET slug = 'legacy-loot-encounter-3-coin-diff-4-ilvl-20', name = 'Purple Legacy Loot Encounter 3 Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Legacy Loot Encounter 3 used for item level 20 crafting.' WHERE id = 283004;
UPDATE items SET slug = 'legacy-loot-encounter-3-coin-diff-5-ilvl-25', name = 'Orange Legacy Loot Encounter 3 Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Legacy Loot Encounter 3 used for item level 25 crafting.' WHERE id = 283005;
UPDATE items SET slug = 'tigrex-raid-coin-ilvl-10', name = 'Green Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 10 crafting.' WHERE id = 290210;
UPDATE items SET slug = 'rathalos-raid-coin-ilvl-10', name = 'Green Rathalos Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 10 crafting.' WHERE id = 290510;
UPDATE items SET slug = 'gypceros-raid-coin-ilvl-10', name = 'Green Gypceros Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 10 crafting.' WHERE id = 290810;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-1-ilvl-1', name = 'Raw Yian Kut-Ku Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 1 crafting.' WHERE id = 292001;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-2-ilvl-10', name = 'Green Yian Kut-Ku Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 10 crafting.' WHERE id = 292002;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-3-ilvl-15', name = 'Blue Yian Kut-Ku Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 15 crafting.' WHERE id = 292003;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-4-ilvl-20', name = 'Purple Yian Kut-Ku Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 20 crafting.' WHERE id = 292004;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-5-ilvl-25', name = 'Orange Yian Kut-Ku Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 25 crafting.' WHERE id = 292005;
UPDATE items SET slug = 'yian-kut-ku-coin-diff-101-ilvl-10', name = 'Green Yian Kut-Ku Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 10 crafting.' WHERE id = 292101;
UPDATE items SET slug = 'rathian-coin-diff-1-ilvl-1', name = 'Raw Rathian Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 1 crafting.' WHERE id = 302001;
UPDATE items SET slug = 'rathian-coin-diff-2-ilvl-10', name = 'Green Rathian Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 10 crafting.' WHERE id = 302002;
UPDATE items SET slug = 'rathian-coin-diff-3-ilvl-15', name = 'Blue Rathian Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 15 crafting.' WHERE id = 302003;
UPDATE items SET slug = 'rathian-coin-diff-4-ilvl-20', name = 'Purple Rathian Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 20 crafting.' WHERE id = 302004;
UPDATE items SET slug = 'rathian-coin-diff-5-ilvl-25', name = 'Orange Rathian Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 25 crafting.' WHERE id = 302005;
UPDATE items SET slug = 'tigrex-dungeon-boss-coin-ilvl-10', name = 'Green Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 10 crafting.' WHERE id = 310310;
UPDATE items SET slug = 'rathalos-dungeon-boss-coin-ilvl-10', name = 'Green Rathalos Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 10 crafting.' WHERE id = 310610;
UPDATE items SET slug = 'gypceros-dungeon-boss-coin-ilvl-10', name = 'Green Gypceros Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 10 crafting.' WHERE id = 310910;
UPDATE items SET slug = 'tigrex-coin-ilvl-10', name = 'Green Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 10 crafting.' WHERE id = 320310;
UPDATE items SET slug = 'tigrex-coin-ilvl-15', name = 'Blue Tigrex Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 15 crafting.' WHERE id = 320315;
UPDATE items SET slug = 'tigrex-coin-ilvl-20', name = 'Purple Tigrex Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 20 crafting.' WHERE id = 320320;
UPDATE items SET slug = 'tigrex-coin-ilvl-25', name = 'Orange Tigrex Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 25 crafting.' WHERE id = 320325;
UPDATE items SET slug = 'azuros-dungeon-boss-coin-ilvl-15', name = 'Blue Azuros Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Azuros used for item level 15 crafting.' WHERE id = 320615;
UPDATE items SET slug = 'diablos-dungeon-boss-coin-ilvl-15', name = 'Blue Diablos Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Diablos used for item level 15 crafting.' WHERE id = 320915;
UPDATE items SET slug = 'nargacuga-raid-boss-coin-ilvl-15', name = 'Blue Nargacuga Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Nargacuga used for item level 15 crafting.' WHERE id = 330315;
UPDATE items SET slug = 'azuros-raid-boss-coin-ilvl-15', name = 'Blue Azuros Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Azuros used for item level 15 crafting.' WHERE id = 330615;
UPDATE items SET slug = 'diablos-raid-boss-coin-ilvl-15', name = 'Blue Diablos Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Diablos used for item level 15 crafting.' WHERE id = 330915;
UPDATE items SET slug = 'barroth-dungeon-boss-coin-ilvl-20', name = 'Purple Barroth Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Barroth used for item level 20 crafting.' WHERE id = 340320;
UPDATE items SET slug = 'tobi-kadachi-dungeon-boss-coin-ilvl-20', name = 'Purple Tobi Kadachi Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tobi Kadachi used for item level 20 crafting.' WHERE id = 340620;
UPDATE items SET slug = 'monoblos-dungeon-boss-coin-ilvl-20', name = 'Purple Monoblos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Monoblos used for item level 20 crafting.' WHERE id = 340920;
UPDATE items SET slug = 'barroth-raid-boss-coin-ilvl-20', name = 'Purple Barroth Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Barroth used for item level 20 crafting.' WHERE id = 350320;
UPDATE items SET slug = 'tobi-kadachi-raid-boss-coin-ilvl-20', name = 'Purple Tobi Kadachi Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tobi Kadachi used for item level 20 crafting.' WHERE id = 350620;
UPDATE items SET slug = 'monoblos-raid-boss-coin-ilvl-20', name = 'Purple Monoblos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Monoblos used for item level 20 crafting.' WHERE id = 350920;
UPDATE items SET slug = 'anjanath-dungeon-boss-coin-ilvl-25', name = 'Orange Anjanath Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Anjanath used for item level 25 crafting.' WHERE id = 360325;
UPDATE items SET slug = 'bazelgeuse-dungeon-boss-coin-ilvl-25', name = 'Orange Bazelgeuse Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bazelgeuse used for item level 25 crafting.' WHERE id = 360625;
UPDATE items SET slug = 'odogaron-dungeon-boss-coin-ilvl-25', name = 'Orange Odogaron Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Odogaron used for item level 25 crafting.' WHERE id = 360925;
UPDATE items SET slug = 'anjanath-raid-boss-coin-ilvl-25', name = 'Orange Anjanath Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Anjanath used for item level 25 crafting.' WHERE id = 370325;
UPDATE items SET slug = 'bazelgeuse-raid-boss-coin-ilvl-25', name = 'Orange Bazelgeuse Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bazelgeuse used for item level 25 crafting.' WHERE id = 370625;
UPDATE items SET slug = 'odogaron-raid-boss-coin-ilvl-25', name = 'Orange Odogaron Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Odogaron used for item level 25 crafting.' WHERE id = 370925;
UPDATE items SET slug = 'tigrex-raid-coin-diff-101-ilvl-10', name = 'Green Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 10 crafting.' WHERE id = 382101;
UPDATE items SET slug = 'bulldrome-boss-coin-diff-1-ilvl-1', name = 'Raw Bulldrome Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 1 crafting.' WHERE id = 383001;
UPDATE items SET slug = 'bulldrome-boss-coin-diff-2-ilvl-10', name = 'Green Bulldrome Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 10 crafting.' WHERE id = 383002;
UPDATE items SET slug = 'bulldrome-boss-coin-diff-3-ilvl-15', name = 'Blue Bulldrome Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 15 crafting.' WHERE id = 383003;
UPDATE items SET slug = 'bulldrome-boss-coin-diff-4-ilvl-20', name = 'Purple Bulldrome Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 20 crafting.' WHERE id = 383004;
UPDATE items SET slug = 'bulldrome-boss-coin-diff-5-ilvl-25', name = 'Orange Bulldrome Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bulldrome used for item level 25 crafting.' WHERE id = 383005;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-1-ilvl-1', name = 'Raw High Inquisitor Vael Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 1 crafting.' WHERE id = 385001;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-2-ilvl-10', name = 'Green High Inquisitor Vael Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 10 crafting.' WHERE id = 385002;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-3-ilvl-15', name = 'Blue High Inquisitor Vael Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 15 crafting.' WHERE id = 385003;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-4-ilvl-20', name = 'Purple High Inquisitor Vael Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 20 crafting.' WHERE id = 385004;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-5-ilvl-25', name = 'Orange High Inquisitor Vael Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 25 crafting.' WHERE id = 385005;
UPDATE items SET slug = 'high-inquisitor-vael-coin-diff-101-ilvl-10', name = 'Green High Inquisitor Vael Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from High Inquisitor Vael used for item level 10 crafting.' WHERE id = 385101;
UPDATE items SET slug = 'the-ember-crown-coin-diff-1-ilvl-1', name = 'Raw The Ember Crown Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 1 crafting.' WHERE id = 388001;
UPDATE items SET slug = 'the-ember-crown-coin-diff-2-ilvl-10', name = 'Green The Ember Crown Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 10 crafting.' WHERE id = 388002;
UPDATE items SET slug = 'the-ember-crown-coin-diff-3-ilvl-15', name = 'Blue The Ember Crown Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 15 crafting.' WHERE id = 388003;
UPDATE items SET slug = 'the-ember-crown-coin-diff-4-ilvl-20', name = 'Purple The Ember Crown Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 20 crafting.' WHERE id = 388004;
UPDATE items SET slug = 'the-ember-crown-coin-diff-5-ilvl-25', name = 'Orange The Ember Crown Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 25 crafting.' WHERE id = 388005;
UPDATE items SET slug = 'the-ember-crown-coin-diff-101-ilvl-10', name = 'Green The Ember Crown Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from The Ember Crown used for item level 10 crafting.' WHERE id = 388101;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-1-ilvl-1', name = 'Raw Yian Kut-Ku Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 1 crafting.' WHERE id = 483001;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-2-ilvl-10', name = 'Green Yian Kut-Ku Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 10 crafting.' WHERE id = 483002;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-3-ilvl-15', name = 'Blue Yian Kut-Ku Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 15 crafting.' WHERE id = 483003;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-4-ilvl-20', name = 'Purple Yian Kut-Ku Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 20 crafting.' WHERE id = 483004;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-5-ilvl-25', name = 'Orange Yian Kut-Ku Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 25 crafting.' WHERE id = 483005;
UPDATE items SET slug = 'yian-kut-ku-boss-coin-diff-101-ilvl-10', name = 'Green Yian Kut-Ku Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Yian Kut-Ku used for item level 10 crafting.' WHERE id = 483101;
UPDATE items SET slug = 'rathian-boss-coin-diff-1-ilvl-1', name = 'Raw Rathian Coin', slot = 'component', rarity = 'common', item_level = 1, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 1 crafting.' WHERE id = 583001;
UPDATE items SET slug = 'rathian-boss-coin-diff-2-ilvl-10', name = 'Green Rathian Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 10 crafting.' WHERE id = 583002;
UPDATE items SET slug = 'rathian-boss-coin-diff-3-ilvl-15', name = 'Blue Rathian Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 15 crafting.' WHERE id = 583003;
UPDATE items SET slug = 'rathian-boss-coin-diff-4-ilvl-20', name = 'Purple Rathian Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 20 crafting.' WHERE id = 583004;
UPDATE items SET slug = 'rathian-boss-coin-diff-5-ilvl-25', name = 'Orange Rathian Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathian used for item level 25 crafting.' WHERE id = 583005;
UPDATE items SET slug = 'tigrex-boss-coin-diff-2-ilvl-10', name = 'Green Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 10 crafting.' WHERE id = 683002;
UPDATE items SET slug = 'tigrex-boss-coin-diff-3-ilvl-15', name = 'Blue Tigrex Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 15 crafting.' WHERE id = 683003;
UPDATE items SET slug = 'tigrex-boss-coin-diff-4-ilvl-20', name = 'Purple Tigrex Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 20 crafting.' WHERE id = 683004;
UPDATE items SET slug = 'tigrex-boss-coin-diff-5-ilvl-25', name = 'Orange Tigrex Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tigrex used for item level 25 crafting.' WHERE id = 683005;
UPDATE items SET slug = 'rathalos-boss-coin-diff-2-ilvl-10', name = 'Green Rathalos Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 10 crafting.' WHERE id = 783002;
UPDATE items SET slug = 'rathalos-boss-coin-diff-3-ilvl-15', name = 'Blue Rathalos Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 15 crafting.' WHERE id = 783003;
UPDATE items SET slug = 'rathalos-boss-coin-diff-4-ilvl-20', name = 'Purple Rathalos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 20 crafting.' WHERE id = 783004;
UPDATE items SET slug = 'rathalos-boss-coin-diff-5-ilvl-25', name = 'Orange Rathalos Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Rathalos used for item level 25 crafting.' WHERE id = 783005;
UPDATE items SET slug = 'nargacuga-raid-boss-coin-diff-103-ilvl-15', name = 'Blue Apex Nargacuga Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Nargacuga used for item level 15 crafting.' WHERE id = 783103;
UPDATE items SET slug = 'azuros-raid-boss-coin-diff-103-ilvl-15', name = 'Blue Apex Azuros Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Azuros used for item level 15 crafting.' WHERE id = 786103;
UPDATE items SET slug = 'diablos-raid-boss-coin-diff-103-ilvl-15', name = 'Blue Apex Diablos Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Diablos used for item level 15 crafting.' WHERE id = 789103;
UPDATE items SET slug = 'gypceros-boss-coin-diff-2-ilvl-10', name = 'Green Gypceros Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 10 crafting.' WHERE id = 883002;
UPDATE items SET slug = 'gypceros-boss-coin-diff-3-ilvl-15', name = 'Blue Gypceros Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 15 crafting.' WHERE id = 883003;
UPDATE items SET slug = 'gypceros-boss-coin-diff-4-ilvl-20', name = 'Purple Gypceros Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 20 crafting.' WHERE id = 883004;
UPDATE items SET slug = 'gypceros-boss-coin-diff-5-ilvl-25', name = 'Orange Gypceros Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Gypceros used for item level 25 crafting.' WHERE id = 883005;
UPDATE items SET slug = 'nargacuga-boss-coin-diff-3-ilvl-15', name = 'Blue Nargacuga Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Nargacuga used for item level 15 crafting.' WHERE id = 983003;
UPDATE items SET slug = 'nargacuga-boss-coin-diff-4-ilvl-20', name = 'Purple Nargacuga Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Nargacuga used for item level 20 crafting.' WHERE id = 983004;
UPDATE items SET slug = 'nargacuga-boss-coin-diff-5-ilvl-25', name = 'Orange Nargacuga Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Nargacuga used for item level 25 crafting.' WHERE id = 983005;
UPDATE items SET slug = 'barroth-raid-boss-coin-diff-104-ilvl-20', name = 'Purple Apex Barroth Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Barroth used for item level 20 crafting.' WHERE id = 983104;
UPDATE items SET slug = 'tobi-kadachi-raid-boss-coin-diff-104-ilvl-20', name = 'Purple Apex Tobi Kadachi Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Tobi Kadachi used for item level 20 crafting.' WHERE id = 986104;
UPDATE items SET slug = 'monoblos-raid-boss-coin-diff-104-ilvl-20', name = 'Purple Apex Monoblos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Monoblos used for item level 20 crafting.' WHERE id = 989104;
UPDATE items SET slug = 'azuros-boss-coin-diff-3-ilvl-15', name = 'Blue Azuros Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Azuros used for item level 15 crafting.' WHERE id = 1083003;
UPDATE items SET slug = 'azuros-boss-coin-diff-4-ilvl-20', name = 'Purple Azuros Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Azuros used for item level 20 crafting.' WHERE id = 1083004;
UPDATE items SET slug = 'azuros-boss-coin-diff-5-ilvl-25', name = 'Orange Azuros Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Azuros used for item level 25 crafting.' WHERE id = 1083005;
UPDATE items SET slug = 'diablos-boss-coin-diff-3-ilvl-15', name = 'Blue Diablos Coin', slot = 'component', rarity = 'rare', item_level = 15, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Diablos used for item level 15 crafting.' WHERE id = 1183003;
UPDATE items SET slug = 'diablos-boss-coin-diff-4-ilvl-20', name = 'Purple Diablos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Diablos used for item level 20 crafting.' WHERE id = 1183004;
UPDATE items SET slug = 'diablos-boss-coin-diff-5-ilvl-25', name = 'Orange Diablos Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Diablos used for item level 25 crafting.' WHERE id = 1183005;
UPDATE items SET slug = 'anjanath-raid-boss-coin-diff-105-ilvl-25', name = 'Orange Apex Anjanath Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Anjanath used for item level 25 crafting.' WHERE id = 1183105;
UPDATE items SET slug = 'bazelgeuse-raid-boss-coin-diff-105-ilvl-25', name = 'Orange Apex Bazelgeuse Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Bazelgeuse used for item level 25 crafting.' WHERE id = 1186105;
UPDATE items SET slug = 'odogaron-raid-boss-coin-diff-105-ilvl-25', name = 'Orange Apex Odogaron Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Odogaron used for item level 25 crafting.' WHERE id = 1189105;
UPDATE items SET slug = 'barroth-boss-coin-diff-4-ilvl-20', name = 'Purple Barroth Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Barroth used for item level 20 crafting.' WHERE id = 1283004;
UPDATE items SET slug = 'barroth-boss-coin-diff-5-ilvl-25', name = 'Orange Barroth Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Barroth used for item level 25 crafting.' WHERE id = 1283005;
UPDATE items SET slug = 'tobi-kadachi-boss-coin-diff-4-ilvl-20', name = 'Purple Tobi Kadachi Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tobi Kadachi used for item level 20 crafting.' WHERE id = 1383004;
UPDATE items SET slug = 'tobi-kadachi-boss-coin-diff-5-ilvl-25', name = 'Orange Tobi Kadachi Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Tobi Kadachi used for item level 25 crafting.' WHERE id = 1383005;
UPDATE items SET slug = 'monoblos-boss-coin-diff-4-ilvl-20', name = 'Purple Monoblos Coin', slot = 'component', rarity = 'epic', item_level = 20, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Monoblos used for item level 20 crafting.' WHERE id = 1483004;
UPDATE items SET slug = 'monoblos-boss-coin-diff-5-ilvl-25', name = 'Orange Monoblos Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Monoblos used for item level 25 crafting.' WHERE id = 1483005;
UPDATE items SET slug = 'anjanath-boss-coin-diff-5-ilvl-25', name = 'Orange Anjanath Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Anjanath used for item level 25 crafting.' WHERE id = 1583005;
UPDATE items SET slug = 'bazelgeuse-boss-coin-diff-5-ilvl-25', name = 'Orange Bazelgeuse Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Bazelgeuse used for item level 25 crafting.' WHERE id = 1683005;
UPDATE items SET slug = 'odogaron-boss-coin-diff-5-ilvl-25', name = 'Orange Odogaron Coin', slot = 'component', rarity = 'legendary', item_level = 25, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Odogaron used for item level 25 crafting.' WHERE id = 1783005;
UPDATE items SET slug = 'tigrex-raid-boss-coin-diff-101-ilvl-10', name = 'Green Apex Tigrex Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Tigrex used for item level 10 crafting.' WHERE id = 2283101;
UPDATE items SET slug = 'rathalos-raid-boss-coin-diff-101-ilvl-10', name = 'Green Apex Rathalos Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Rathalos used for item level 10 crafting.' WHERE id = 2286101;
UPDATE items SET slug = 'gypceros-raid-boss-coin-diff-101-ilvl-10', name = 'Green Apex Gypceros Coin', slot = 'component', rarity = 'uncommon', item_level = 10, healing_power = 0, max_resource_bonus = 0, glyph = '$', image_url = '/equipment-placeholder.svg', description = 'A boss coin from Apex Gypceros used for item level 10 crafting.' WHERE id = 2289101;
DELETE FROM encounter_loot;
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (103, 383001, 1, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (103, 383002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (103, 383003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (103, 383004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (103, 383005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (203, 483001, 1, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (203, 483002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (203, 483003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (203, 483004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (203, 483005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (303, 583001, 1, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (303, 583002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (303, 583003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (303, 583004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (303, 583005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (403, 683002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (403, 683003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (403, 683004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (403, 683005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (503, 783002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (503, 783003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (503, 783004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (503, 783005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (603, 883002, 2, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (603, 883003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (603, 883004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (603, 883005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (703, 983003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (703, 983004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (703, 983005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (803, 1083003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (803, 1083004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (803, 1083005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (903, 1183003, 3, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (903, 1183004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (903, 1183005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1003, 1283004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1003, 1283005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1103, 1383004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1103, 1383005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1203, 1483004, 4, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1203, 1483005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1303, 1583005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1403, 1683005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (1503, 1783005, 5, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (2003, 2283101, 101, 100, 1);
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2103, id, 101, 100, 1 FROM items WHERE slug = 'rathalos-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2203, id, 101, 100, 1 FROM items WHERE slug = 'gypceros-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2303, id, 103, 100, 1 FROM items WHERE slug = 'nargacuga-raid-boss-coin-diff-103-ilvl-15';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2403, id, 103, 100, 1 FROM items WHERE slug = 'azuros-raid-boss-coin-diff-103-ilvl-15';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2503, id, 103, 100, 1 FROM items WHERE slug = 'diablos-raid-boss-coin-diff-103-ilvl-15';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2603, id, 104, 100, 1 FROM items WHERE slug = 'barroth-raid-boss-coin-diff-104-ilvl-20';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2703, id, 104, 100, 1 FROM items WHERE slug = 'tobi-kadachi-raid-boss-coin-diff-104-ilvl-20';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2803, id, 104, 100, 1 FROM items WHERE slug = 'monoblos-raid-boss-coin-diff-104-ilvl-20';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 2903, id, 105, 100, 1 FROM items WHERE slug = 'anjanath-raid-boss-coin-diff-105-ilvl-25';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 3003, id, 105, 100, 1 FROM items WHERE slug = 'bazelgeuse-raid-boss-coin-diff-105-ilvl-25';
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) SELECT 3103, id, 105, 100, 1 FROM items WHERE slug = 'odogaron-raid-boss-coin-diff-105-ilvl-25';
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 1, source_encounter_id = 103 WHERE id = 1001;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 1, source_encounter_id = 103 WHERE id = 1002;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 1, source_encounter_id = 103 WHERE id = 1003;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 2, source_encounter_id = 203 WHERE id = 1004;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 2, source_encounter_id = 203 WHERE id = 1005;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 2, source_encounter_id = 203 WHERE id = 1006;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 3, source_encounter_id = 303 WHERE id = 1007;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 3, source_encounter_id = 303 WHERE id = 1008;
UPDATE crafting_recipes SET difficulty_id = 1, source_dungeon_id = 3, source_encounter_id = 303 WHERE id = 1009;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 4, source_encounter_id = 403 WHERE id = 1101;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 4, source_encounter_id = 403 WHERE id = 1102;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 4, source_encounter_id = 403 WHERE id = 1103;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 5, source_encounter_id = 503 WHERE id = 1104;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 5, source_encounter_id = 503 WHERE id = 1105;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 5, source_encounter_id = 503 WHERE id = 1106;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 6, source_encounter_id = 603 WHERE id = 1107;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 6, source_encounter_id = 603 WHERE id = 1108;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 6, source_encounter_id = 603 WHERE id = 1109;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 7, source_encounter_id = 703 WHERE id = 1201;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 7, source_encounter_id = 703 WHERE id = 1202;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 7, source_encounter_id = 703 WHERE id = 1203;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 8, source_encounter_id = 803 WHERE id = 1204;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 8, source_encounter_id = 803 WHERE id = 1205;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 8, source_encounter_id = 803 WHERE id = 1206;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 9, source_encounter_id = 903 WHERE id = 1207;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 9, source_encounter_id = 903 WHERE id = 1208;
UPDATE crafting_recipes SET difficulty_id = 2, source_dungeon_id = 9, source_encounter_id = 903 WHERE id = 1209;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 10, source_encounter_id = 1003 WHERE id = 1301;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 10, source_encounter_id = 1003 WHERE id = 1302;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 10, source_encounter_id = 1003 WHERE id = 1303;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 11, source_encounter_id = 1103 WHERE id = 1304;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 11, source_encounter_id = 1103 WHERE id = 1305;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 11, source_encounter_id = 1103 WHERE id = 1306;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 12, source_encounter_id = 1203 WHERE id = 1307;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 12, source_encounter_id = 1203 WHERE id = 1308;
UPDATE crafting_recipes SET difficulty_id = 4, source_dungeon_id = 12, source_encounter_id = 1203 WHERE id = 1309;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 13, source_encounter_id = 1303 WHERE id = 1401;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 13, source_encounter_id = 1303 WHERE id = 1402;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 13, source_encounter_id = 1303 WHERE id = 1403;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 14, source_encounter_id = 1403 WHERE id = 1404;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 14, source_encounter_id = 1403 WHERE id = 1405;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 14, source_encounter_id = 1403 WHERE id = 1406;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 15, source_encounter_id = 1503 WHERE id = 1407;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 15, source_encounter_id = 1503 WHERE id = 1408;
UPDATE crafting_recipes SET difficulty_id = 5, source_dungeon_id = 15, source_encounter_id = 1503 WHERE id = 1409;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 20, source_encounter_id = 2003 WHERE id = 2001;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 20, source_encounter_id = 2003 WHERE id = 2002;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 20, source_encounter_id = 2003 WHERE id = 2003;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 21, source_encounter_id = 2103 WHERE id = 2004;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 21, source_encounter_id = 2103 WHERE id = 2005;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 21, source_encounter_id = 2103 WHERE id = 2006;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 22, source_encounter_id = 2203 WHERE id = 2007;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 22, source_encounter_id = 2203 WHERE id = 2008;
UPDATE crafting_recipes SET difficulty_id = 101, source_dungeon_id = 22, source_encounter_id = 2203 WHERE id = 2009;
DELETE FROM crafting_recipe_components;
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1001, 383001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1002, 383001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1003, 383001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1004, 483001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1005, 483001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1006, 483001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1007, 583001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1008, 583001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1009, 583001, 5);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 683002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 683002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 683002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 783002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 783002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 783002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 983003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 983003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 983003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 1083003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1205, 1083003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1206, 1083003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1207, 1183003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1208, 1183003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1209, 1183003, 15);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 1283004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 1283004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 1283004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1304, 1383004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1305, 1383004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1306, 1383004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1307, 1483004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1308, 1483004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1309, 1483004, 20);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1401, 1583005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1402, 1583005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1403, 1583005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1404, 1683005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1405, 1683005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1406, 1683005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1407, 1783005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1408, 1783005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1409, 1783005, 25);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (2001, 2283101, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (2002, 2283101, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (2003, 2283101, 10);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2004, id, 10 FROM items WHERE slug = 'rathalos-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2005, id, 10 FROM items WHERE slug = 'rathalos-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2006, id, 10 FROM items WHERE slug = 'rathalos-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2007, id, 10 FROM items WHERE slug = 'gypceros-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2008, id, 10 FROM items WHERE slug = 'gypceros-raid-boss-coin-diff-101-ilvl-10';
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) SELECT 2009, id, 10 FROM items WHERE slug = 'gypceros-raid-boss-coin-diff-101-ilvl-10';
DELETE FROM gear_upgrade_paths;
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (1, 201);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (2, 202);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (3, 203);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (4, 204);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (5, 205);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (6, 206);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (7, 207);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (8, 208);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (9, 209);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (100, 3);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (101, 5);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (102, 2);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (103, 6);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (104, 4);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (105, 1);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (106, 7);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (107, 3);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (108, 8);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (109, 9);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (201, 301);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (202, 302);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (203, 303);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (204, 304);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (205, 305);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (206, 306);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (207, 307);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (208, 308);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (209, 309);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (301, 401);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (302, 402);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (303, 403);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (304, 404);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (305, 405);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (306, 406);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (307, 407);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (308, 408);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (309, 409);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (401, 501);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (402, 502);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (403, 503);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (404, 504);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (405, 505);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (406, 506);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (407, 507);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (408, 508);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (409, 509);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (710, 301);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (711, 302);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (712, 303);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (713, 304);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (714, 305);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (715, 306);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (716, 307);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (717, 308);
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (718, 309);
COMMIT;
+8 -1
View File
@@ -14,9 +14,10 @@ CREATE TABLE IF NOT EXISTS dungeons (
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,
party_size INTEGER NOT NULL DEFAULT 6,
completion_item_level INTEGER,
experience_reward INTEGER NOT NULL DEFAULT 100,
image_url TEXT NOT NULL DEFAULT '/boss-placeholder.svg',
description TEXT NOT NULL
);
@@ -132,6 +133,12 @@ CREATE TABLE IF NOT EXISTS crafting_recipe_components (
PRIMARY KEY (recipe_id, item_id)
);
CREATE TABLE IF NOT EXISTS gear_upgrade_paths (
from_item_id INTEGER PRIMARY KEY REFERENCES items(id) ON DELETE CASCADE,
to_item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
CHECK (from_item_id <> to_item_id)
);
CREATE TABLE IF NOT EXISTS item_sets (
id INTEGER PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
+1005 -69
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
# Gearing System
## Current Rule
The game uses fewer playable content tiers and more gear upgrade steps.
Content tiers:
| Content Tier | Unlock Level | Purpose |
| --- | ---: | --- |
| iLvl 1 | 1 | First real gear set |
| iLvl 10 | 10 | Midgame jump |
| iLvl 20 | 20 | Endgame jump |
| iLvl 25 | 25 | Hard endgame |
Gear tiers:
| Gear Tier | How It Is Used |
| ---: | --- |
| 1 | Crafted from iLvl 1 content |
| 5 | Upgrade tier from iLvl 1 content coins |
| 10 | Crafted/upgraded from iLvl 10 content coins |
| 15 | Gear-only upgrade tier from iLvl 10 content coins |
| 20 | Crafted/upgraded from iLvl 20 content coins |
| 25 | Crafted/upgraded from iLvl 25 content coins |
This keeps the dungeon/raid picker simple while still giving players steady gear goals.
## New Characters
New characters start with no gear equipped.
The first goal is to clear iLvl 1 content, earn raw boss coins, and craft the first iLvl 1 set. Starter gear exists as craftable gear, not automatic inventory.
## Coin Tiers
Coins are component items. Each coin is tied to a boss and a content tier.
| Content Tier | Coin Prefix | Rarity Key | Example |
| ---: | --- | --- | --- |
| 1 | Raw | common | Raw Bulldrome Coin |
| 10 | Green | uncommon | Green Bulldrome Coin |
| 20 | Purple | epic | Purple Bulldrome Coin |
| 25 | Orange | legendary | Orange Bulldrome Coin |
iLvl 5 and iLvl 15 gear do not have their own playable content tier. They use coins from the previous playable tier:
| Gear Tier | Coin Tier Used |
| ---: | ---: |
| 1 | 1 |
| 5 | 1 |
| 10 | 10 |
| 15 | 10 |
| 20 | 20 |
| 25 | 25 |
## Boss Loot
Each boss drops one boss coin for the selected content tier.
Examples:
- Bulldrome at iLvl 1 drops Raw Bulldrome Coins.
- Tigrex at iLvl 10 drops Green Tigrex Coins.
- Barroth at iLvl 20 drops Purple Barroth Coins.
- Anjanath at iLvl 25 drops Orange Anjanath Coins.
## Crafting And Upgrades
The first gear item in a boss/item line can be crafted directly. Higher versions should be reached through Upgrade.
Current rule:
- Craft iLvl 1 boss gear directly.
- Upgrade iLvl 1 -> 5 with iLvl 1 coins.
- Craft or upgrade iLvl 10 gear from iLvl 10 content.
- Upgrade iLvl 10 -> 15 with iLvl 10 coins.
- Craft or upgrade iLvl 20 gear from iLvl 20 content.
- Craft or upgrade iLvl 25 gear from iLvl 25 content.
Upgrade consumes the old item and awards the upgraded item. This avoids duplicate clutter and keeps item identity clear.
Examples:
| Upgrade | Cost Source |
| --- | --- |
| Raw Bulldrome Helmet iLvl 1 -> Honed Bulldrome Helmet iLvl 5 | Raw Bulldrome Coins |
| Green Tigrex Helmet iLvl 10 -> Blue Tigrex Helmet iLvl 15 | Green Tigrex Coins |
| Purple Bulldrome Helmet iLvl 20 -> Orange Bulldrome Helmet iLvl 25 | Orange Bulldrome Coins |
## UI Behavior
The dungeon and raid picker only shows playable content tiers:
```text
iLvl 1 -> iLvl 10 -> iLvl 20 -> iLvl 25
```
The equipment screen still shows gear recipes for:
```text
iLvl 1 -> iLvl 5 -> iLvl 10 -> iLvl 15 -> iLvl 20 -> iLvl 25
```
Direct crafting is blocked for recipes that have a lower item-level version in the same boss/item line. Use the selected item's Upgrade button for those.
## Roguelike Loot
Roguelike gear should follow the same tier brackets.
Recommended mapping:
| Stage Band | Coin Tier |
| --- | ---: |
| 1-4 | 1 |
| 5-9 | 10 |
| 10-14 | 10 |
| 15-19 | 20 |
| 20+ | 25 |
Boss-based coins are still preferred. A roguelike boss should award coins for its actual boss template when possible.
## Data Notes
Authoritative gearing data lives in SQLite seed data:
- `db/seed.sql`
- `src/offline-starter-profile.json`
Run this after changing seed data:
```sh
npm run db:init
npm run offline:export
```
`npm run build` already runs `offline:export`, so a production build also refreshes the offline starter profile.
TrueNAS keeps its own persistent `data/game.db`. Pushing code does not merge or replace that database. The TrueNAS app applies seed/schema changes when the container starts and runs `npm run db:init`.
+164
View File
@@ -0,0 +1,164 @@
# Push Updates
Use this when pushing code from the Mac to `git.whoagland.com`, then updating the TrueNAS-hosted server.
## Rules
- Git deploys code only.
- TrueNAS save data lives in `/mnt/usbssds/apps/iwanttoheal/data/game.db`.
- Do not commit, copy, or replace `data/game.db`.
- Do not run character reset commands unless you intentionally want a wipe.
- Restarting the TrueNAS app runs `npm ci`, `npm run db:init`, `npm run build`, and `npm start`.
## Step 1: Build Web Locally
```sh
cd /Users/warren/Documents/testgame/testgame
npm run build
```
This validates TypeScript, builds the browser app, and refreshes `src/offline-starter-profile.json`.
## Step 2: Optional Android APK
Only run this when building a new APK.
```sh
set -e
cd /Users/warren/Documents/testgame/testgame
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
VERSION="1.0.27"
CURRENT_CODE=$(grep -E 'versionCode [0-9]+' android/app/build.gradle | awk '{print $2}')
NEXT_CODE=$((CURRENT_CODE + 1))
perl -0pi -e "s/versionCode\s+\d+/versionCode $NEXT_CODE/" android/app/build.gradle
perl -0pi -e "s/versionName\s+\"[^\"]+\"/versionName \"$VERSION\"/" android/app/build.gradle
VITE_API_BASE_URL="https://iwanttoheal.phenomrom.com" npm run android:sync
cd android
./gradlew clean assembleDebug
cd ..
cp android/app/build/outputs/apk/debug/app-debug.apk "IWantToHeal-Thor-v$VERSION.apk"
ls -lh "IWantToHeal-Thor-v$VERSION.apk"
```
## Step 3: Commit And Push Code
```sh
cd /Users/warren/Documents/testgame/testgame
git add .
git commit -m "Update game 1.0.27"
git push origin main
```
Check before committing:
```sh
git status --short
```
Expected: source/doc/build-output changes only. Do not stage `data/game.db`.
## Step 4: Optional Gitea Release For APK
Only run this when Step 2 created a new APK.
```sh
set -e
cd /Users/warren/Documents/testgame/testgame
export GITEA_URL="https://git.whoagland.com"
export GITEA_OWNER="phenom"
export GITEA_REPO="i-want-to-heal"
export GITEA_TOKEN="ed2db3fd54546e9658377d0551b3fc3961583f1d"
VERSION="1.0.27"
APK="IWantToHeal-Thor-v$VERSION.apk"
RELEASE_JSON=$(curl -sS -X POST "$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"v$VERSION\",\"target_commitish\":\"main\",\"name\":\"v$VERSION\",\"body\":\"I Want to Heal Android build v$VERSION\",\"draft\":false,\"prerelease\":false}")
RELEASE_ID=$(python3 -c 'import sys,json; data=json.load(sys.stdin); print(data.get("id") or data)' <<< "$RELEASE_JSON")
curl -sS -X POST "$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/releases/$RELEASE_ID/assets?name=$APK" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$APK"
```
## Step 5: Update TrueNAS
```sh
cd /mnt/usbssds/apps/iwanttoheal/app
git pull
```
Before restarting, make a DB backup:
```sh
cp /mnt/usbssds/apps/iwanttoheal/data/game.db \
"/mnt/usbssds/apps/iwanttoheal/data/game-before-update-$(date +%Y%m%d-%H%M%S).db"
```
Then restart the `iwanttoheal` app in the TrueNAS Apps UI.
## What Happens On Restart
The app command runs:
```sh
npm ci && npm run db:init && npm run build && npm start
```
That means:
- dependency changes apply
- schema changes apply
- seed/static-content updates apply
- browser files rebuild
- existing accounts and characters stay in `data/game.db`
`npm run db:init` should not wipe saves. Character wipes are separate manual reset operations.
## Resetting TrueNAS Characters
Only run a reset when intentionally starting everyone over.
Resetting the Mac database does not reset TrueNAS. TrueNAS has its own persistent DB at:
```text
/mnt/usbssds/apps/iwanttoheal/data/game.db
```
Back it up first, then run the reset command or reset SQL on TrueNAS.
## If Something Looks Wrong
Check the mounted DB path:
```sh
ls -lh /mnt/usbssds/apps/iwanttoheal/data/game.db
```
Check the latest code:
```sh
cd /mnt/usbssds/apps/iwanttoheal/app
git log --oneline -5
```
Check the app API:
```sh
curl http://127.0.0.1:4173/api/auth/session
```
+60
View File
@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<rect width="1280" height="720" fill="#111218"/>
<rect x="48" y="36" width="1184" height="648" fill="#1b1c24" stroke="#090a0d" stroke-width="4"/>
<text x="78" y="78" fill="#8f90a0" font-family="monospace" font-size="18">ADVENTURE</text>
<text x="78" y="114" fill="#f2f0dc" font-family="monospace" font-size="34" font-weight="700">Dungeons</text>
<text x="1075" y="88" fill="#e5b95f" font-family="monospace" font-size="18">B Back</text>
<text x="78" y="158" fill="#e5b95f" font-family="monospace" font-size="18">Pick Dungeon</text>
<g>
<rect x="78" y="178" width="335" height="128" fill="#24262f" stroke="#e5b95f" stroke-width="5"/>
<rect x="94" y="194" width="72" height="72" fill="#481e29" stroke="#090a0d" stroke-width="3"/>
<text x="116" y="241" fill="#ef6574" font-family="monospace" font-size="28" font-weight="700">AH</text>
<text x="184" y="216" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Ashen Halls</text>
<text x="184" y="246" fill="#aaa9b7" font-family="monospace" font-size="16">Level 1 • 6 Players</text>
<text x="184" y="276" fill="#e5b95f" font-family="monospace" font-size="16">Selected</text>
</g>
<g>
<rect x="436" y="178" width="335" height="128" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="452" y="194" width="72" height="72" fill="#2a263f" stroke="#090a0d" stroke-width="3"/>
<text x="474" y="241" fill="#8ca9ff" font-family="monospace" font-size="28" font-weight="700">SC</text>
<text x="542" y="216" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Sunken Crypt</text>
<text x="542" y="246" fill="#aaa9b7" font-family="monospace" font-size="16">Level 5 • 6 Players</text>
</g>
<g opacity="0.65">
<rect x="794" y="178" width="335" height="128" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="810" y="194" width="72" height="72" fill="#223226" stroke="#090a0d" stroke-width="3"/>
<text x="832" y="241" fill="#70d990" font-family="monospace" font-size="28" font-weight="700">GM</text>
<text x="900" y="216" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Grove Maw</text>
<text x="900" y="246" fill="#aaa9b7" font-family="monospace" font-size="16">Level 10 • 6 Players</text>
</g>
<text x="78" y="356" fill="#e5b95f" font-family="monospace" font-size="18">Pick Part</text>
<g>
<rect x="78" y="376" width="282" height="82" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<text x="112" y="426" fill="#f2f0dc" font-family="monospace" font-size="24" font-weight="700">Part 1</text>
<text x="250" y="426" fill="#8f90a0" font-family="monospace" font-size="16">3 fights</text>
</g>
<g>
<rect x="382" y="376" width="282" height="82" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="416" y="426" fill="#f2f0dc" font-family="monospace" font-size="24" font-weight="700">Part 2</text>
<text x="554" y="426" fill="#8f90a0" font-family="monospace" font-size="16">Locked</text>
</g>
<g>
<rect x="686" y="376" width="282" height="82" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="720" y="426" fill="#f2f0dc" font-family="monospace" font-size="24" font-weight="700">Part 3</text>
<text x="858" y="426" fill="#8f90a0" font-family="monospace" font-size="16">Locked</text>
</g>
<text x="78" y="508" fill="#e5b95f" font-family="monospace" font-size="18">Pick Difficulty</text>
<rect x="78" y="528" width="240" height="64" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<text x="116" y="568" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Normal</text>
<rect x="342" y="528" width="240" height="64" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="380" y="568" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Heroic</text>
<rect x="606" y="528" width="240" height="64" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="644" y="568" fill="#777988" font-family="monospace" font-size="20" font-weight="700">Mythic L10</text>
<rect x="914" y="508" width="238" height="86" fill="#3a2618" stroke="#e5b95f" stroke-width="5"/>
<text x="982" y="559" fill="#f2f0dc" font-family="monospace" font-size="24" font-weight="700">Start</text>
<text x="78" y="638" fill="#aaa9b7" font-family="monospace" font-size="17">Idea A: All choices are button grids. D-pad works everywhere. No native dropdown.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<rect width="1280" height="720" fill="#111218"/>
<rect x="48" y="36" width="1184" height="648" fill="#1b1c24" stroke="#090a0d" stroke-width="4"/>
<text x="78" y="78" fill="#8f90a0" font-family="monospace" font-size="18">ADVENTURE</text>
<text x="78" y="114" fill="#f2f0dc" font-family="monospace" font-size="34" font-weight="700">Dungeons</text>
<text x="1014" y="88" fill="#e5b95f" font-family="monospace" font-size="18">LB/RB Change</text>
<rect x="78" y="150" width="760" height="398" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="112" y="184" width="164" height="164" fill="#481e29" stroke="#090a0d" stroke-width="4"/>
<text x="151" y="280" fill="#ef6574" font-family="monospace" font-size="48" font-weight="700">AH</text>
<text x="306" y="202" fill="#8f90a0" font-family="monospace" font-size="18">CURRENT DUNGEON</text>
<text x="306" y="248" fill="#f2f0dc" font-family="monospace" font-size="34" font-weight="700">Ashen Halls</text>
<text x="306" y="286" fill="#aaa9b7" font-family="monospace" font-size="18">Guide a six-player party through burning halls.</text>
<text x="306" y="324" fill="#e5b95f" font-family="monospace" font-size="18">Level 1 • 6 Players • 100 XP</text>
<rect x="112" y="384" width="690" height="104" fill="#15161c" stroke="#33343d" stroke-width="3"/>
<text x="138" y="425" fill="#f2f0dc" font-family="monospace" font-size="19">Ashen Halls</text>
<text x="356" y="425" fill="#8f90a0" font-family="monospace" font-size="19">Sunken Crypt</text>
<text x="608" y="425" fill="#8f90a0" font-family="monospace" font-size="19">Grove Maw</text>
<rect x="128" y="448" width="146" height="8" fill="#e5b95f"/>
<rect x="874" y="150" width="278" height="398" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="906" y="194" fill="#e5b95f" font-family="monospace" font-size="18">Setup</text>
<text x="906" y="232" fill="#f2f0dc" font-family="monospace" font-size="22" font-weight="700">Part</text>
<rect x="906" y="252" width="68" height="54" fill="#29291f" stroke="#e5b95f" stroke-width="4"/>
<text x="932" y="286" fill="#f2f0dc" font-family="monospace" font-size="20">1</text>
<rect x="990" y="252" width="68" height="54" fill="#20222a" stroke="#41404a" stroke-width="3"/>
<text x="1016" y="286" fill="#8f90a0" font-family="monospace" font-size="20">2</text>
<rect x="1074" y="252" width="68" height="54" fill="#20222a" stroke="#41404a" stroke-width="3"/>
<text x="1100" y="286" fill="#8f90a0" font-family="monospace" font-size="20">3</text>
<text x="906" y="350" fill="#f2f0dc" font-family="monospace" font-size="22" font-weight="700">Difficulty</text>
<rect x="906" y="372" width="236" height="52" fill="#29291f" stroke="#e5b95f" stroke-width="4"/>
<text x="938" y="405" fill="#f2f0dc" font-family="monospace" font-size="18">Normal</text>
<rect x="906" y="436" width="236" height="52" fill="#20222a" stroke="#41404a" stroke-width="3"/>
<text x="938" y="469" fill="#aaa9b7" font-family="monospace" font-size="18">Heroic</text>
<rect x="78" y="580" width="238" height="70" fill="#15161c" stroke="#41404a" stroke-width="4"/>
<text x="120" y="624" fill="#e5b95f" font-family="monospace" font-size="22">Prev</text>
<rect x="342" y="580" width="238" height="70" fill="#15161c" stroke="#41404a" stroke-width="4"/>
<text x="392" y="624" fill="#e5b95f" font-family="monospace" font-size="22">Next</text>
<rect x="874" y="580" width="278" height="70" fill="#3a2618" stroke="#e5b95f" stroke-width="5"/>
<text x="963" y="624" fill="#f2f0dc" font-family="monospace" font-size="24" font-weight="700">Start</text>
<text x="78" y="684" fill="#aaa9b7" font-family="monospace" font-size="17">Idea B: One big focused dungeon. Shoulder buttons or side buttons cycle dungeon.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<rect width="1280" height="720" fill="#111218"/>
<rect x="48" y="36" width="1184" height="648" fill="#1b1c24" stroke="#090a0d" stroke-width="4"/>
<text x="78" y="78" fill="#8f90a0" font-family="monospace" font-size="18">ADVENTURE</text>
<text x="78" y="114" fill="#f2f0dc" font-family="monospace" font-size="34" font-weight="700">Mission Board</text>
<text x="1046" y="88" fill="#e5b95f" font-family="monospace" font-size="18">A Start</text>
<rect x="78" y="150" width="500" height="474" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="112" y="194" fill="#e5b95f" font-family="monospace" font-size="18">Available Runs</text>
<g>
<rect x="112" y="222" width="432" height="82" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<text x="136" y="255" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Ashen Halls • Part 1</text>
<text x="136" y="283" fill="#aaa9b7" font-family="monospace" font-size="16">Normal • iLvl 1 • 100 XP</text>
</g>
<g>
<rect x="112" y="318" width="432" height="82" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="136" y="351" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Ashen Halls • Part 1</text>
<text x="136" y="379" fill="#aaa9b7" font-family="monospace" font-size="16">Heroic • iLvl 5 • 140 XP</text>
</g>
<g opacity="0.65">
<rect x="112" y="414" width="432" height="82" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="136" y="447" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Sunken Crypt • Part 1</text>
<text x="136" y="475" fill="#aaa9b7" font-family="monospace" font-size="16">Normal • Locked Level 5</text>
</g>
<g opacity="0.65">
<rect x="112" y="510" width="432" height="82" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="136" y="543" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Ashen Halls • Part 2</text>
<text x="136" y="571" fill="#aaa9b7" font-family="monospace" font-size="16">Normal • Locked until Part 1 clear</text>
</g>
<rect x="616" y="150" width="536" height="474" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="650" y="184" width="130" height="130" fill="#481e29" stroke="#090a0d" stroke-width="4"/>
<text x="684" y="262" fill="#ef6574" font-family="monospace" font-size="42" font-weight="700">AH</text>
<text x="812" y="194" fill="#8f90a0" font-family="monospace" font-size="18">SELECTED RUN</text>
<text x="812" y="238" fill="#f2f0dc" font-family="monospace" font-size="30" font-weight="700">Ashen Halls</text>
<text x="812" y="278" fill="#e5b95f" font-family="monospace" font-size="20">Part 1 • Normal</text>
<text x="650" y="358" fill="#aaa9b7" font-family="monospace" font-size="17">Fastest path for controller play. One list item is the exact run.</text>
<text x="650" y="394" fill="#aaa9b7" font-family="monospace" font-size="17">No separate dungeon, phase, or difficulty controls.</text>
<rect x="650" y="440" width="470" height="64" fill="#15161c" stroke="#33343d" stroke-width="3"/>
<text x="680" y="480" fill="#f2f0dc" font-family="monospace" font-size="18">Health 1.00x Damage 1.00x XP 1.0x</text>
<rect x="650" y="532" width="220" height="62" fill="#3a2618" stroke="#e5b95f" stroke-width="5"/>
<text x="716" y="570" fill="#f2f0dc" font-family="monospace" font-size="22" font-weight="700">Start</text>
<rect x="900" y="532" width="220" height="62" fill="#15161c" stroke="#41404a" stroke-width="4"/>
<text x="955" y="570" fill="#e5b95f" font-family="monospace" font-size="22">Loot</text>
<text x="78" y="684" fill="#aaa9b7" font-family="monospace" font-size="17">Idea C: Flat mission list. Most controller-friendly, least setup flexibility on one screen.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<rect width="1280" height="720" fill="#111218"/>
<rect x="48" y="36" width="1184" height="648" fill="#1b1c24" stroke="#090a0d" stroke-width="4"/>
<text x="78" y="78" fill="#8f90a0" font-family="monospace" font-size="18">ADVENTURE</text>
<text x="78" y="114" fill="#f2f0dc" font-family="monospace" font-size="34" font-weight="700">Dungeons</text>
<text x="958" y="88" fill="#e5b95f" font-family="monospace" font-size="18">B Back • A Select</text>
<text x="78" y="158" fill="#e5b95f" font-family="monospace" font-size="18">1. Pick Item Level</text>
<g>
<rect x="78" y="178" width="170" height="72" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="125" y="222" fill="#f2f0dc" font-family="monospace" font-size="22">5</text>
<text x="108" y="240" fill="#8f90a0" font-family="monospace" font-size="12">Initiate</text>
</g>
<g>
<rect x="268" y="178" width="170" height="72" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="310" y="222" fill="#f2f0dc" font-family="monospace" font-size="22">10</text>
<text x="300" y="240" fill="#8f90a0" font-family="monospace" font-size="12">Veteran</text>
</g>
<g>
<rect x="458" y="178" width="170" height="72" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="500" y="222" fill="#f2f0dc" font-family="monospace" font-size="22">15</text>
<text x="488" y="240" fill="#8f90a0" font-family="monospace" font-size="12">Champion</text>
</g>
<g>
<rect x="648" y="178" width="170" height="72" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<text x="690" y="222" fill="#f2f0dc" font-family="monospace" font-size="22" font-weight="700">20</text>
<text x="690" y="240" fill="#e5b95f" font-family="monospace" font-size="12">Mythic</text>
</g>
<g opacity="0.65">
<rect x="838" y="178" width="170" height="72" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="880" y="222" fill="#777988" font-family="monospace" font-size="22">25</text>
<text x="864" y="240" fill="#777988" font-family="monospace" font-size="12">Level 20</text>
</g>
<text x="78" y="304" fill="#e5b95f" font-family="monospace" font-size="18">2. Pick Run</text>
<g>
<rect x="78" y="324" width="335" height="154" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<rect x="96" y="346" width="64" height="64" fill="#481e29" stroke="#090a0d" stroke-width="3"/>
<text x="114" y="387" fill="#ef6574" font-family="monospace" font-size="24" font-weight="700">AH</text>
<text x="178" y="360" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Ashen Halls</text>
<text x="178" y="389" fill="#aaa9b7" font-family="monospace" font-size="15">Mythic • iLvl 20</text>
<text x="96" y="446" fill="#e5b95f" font-family="monospace" font-size="16">Part 1 unlocked</text>
</g>
<g>
<rect x="436" y="324" width="335" height="154" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="454" y="346" width="64" height="64" fill="#2a263f" stroke="#090a0d" stroke-width="3"/>
<text x="472" y="387" fill="#8ca9ff" font-family="monospace" font-size="24" font-weight="700">SC</text>
<text x="536" y="360" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Sunken Crypt</text>
<text x="536" y="389" fill="#aaa9b7" font-family="monospace" font-size="15">Mythic • iLvl 20</text>
<text x="454" y="446" fill="#aaa9b7" font-family="monospace" font-size="16">Part 1 unlocked</text>
</g>
<g>
<rect x="794" y="324" width="335" height="154" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<rect x="812" y="346" width="64" height="64" fill="#223226" stroke="#090a0d" stroke-width="3"/>
<text x="830" y="387" fill="#70d990" font-family="monospace" font-size="24" font-weight="700">GM</text>
<text x="894" y="360" fill="#f2f0dc" font-family="monospace" font-size="20" font-weight="700">Grove Maw</text>
<text x="894" y="389" fill="#aaa9b7" font-family="monospace" font-size="15">Mythic • iLvl 20</text>
<text x="812" y="446" fill="#777988" font-family="monospace" font-size="16">Locked dungeon</text>
</g>
<text x="78" y="532" fill="#e5b95f" font-family="monospace" font-size="18">3. Pick Part</text>
<rect x="78" y="552" width="250" height="64" fill="#29291f" stroke="#e5b95f" stroke-width="5"/>
<text x="124" y="592" fill="#f2f0dc" font-family="monospace" font-size="20">Part 1</text>
<rect x="352" y="552" width="250" height="64" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="398" y="592" fill="#aaa9b7" font-family="monospace" font-size="20">Part 2</text>
<rect x="626" y="552" width="250" height="64" fill="#20222a" stroke="#41404a" stroke-width="4"/>
<text x="672" y="592" fill="#aaa9b7" font-family="monospace" font-size="20">Part 3</text>
<rect x="926" y="552" width="226" height="64" fill="#3a2618" stroke="#e5b95f" stroke-width="5"/>
<text x="991" y="592" fill="#f2f0dc" font-family="monospace" font-size="22" font-weight="700">Start</text>
<text x="78" y="662" fill="#aaa9b7" font-family="monospace" font-size="17">Idea D: submenu flow. Pick item level first, then only compatible dungeon cards appear.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" width="620" height="540" viewBox="0 0 620 540" role="img" aria-label="Ayn Thor secondary display spell effect quick swap mockup">
<rect width="620" height="540" fill="#111219"/>
<rect x="16" y="16" width="588" height="48" fill="#20222d" stroke="#0a0b0e" stroke-width="3"/>
<text x="32" y="36" fill="#8aa0b7" font-family="monospace" font-size="11" font-weight="700">SPELL EFFECTS</text>
<text x="32" y="56" fill="#f5e6b2" font-family="monospace" font-size="18" font-weight="900">Quick Swap</text>
<text x="460" y="47" fill="#83d99b" font-family="monospace" font-size="13" font-weight="900">4/4 ACTIVE</text>
<g transform="translate(16 82)">
<rect width="588" height="118" fill="#191b25" stroke="#3a3944" stroke-width="2"/>
<text x="16" y="28" fill="#8aa0b7" font-family="monospace" font-size="10" font-weight="700">ACTIVE SLOTS</text>
<g transform="translate(14 46)">
<rect width="130" height="54" fill="#29291f" stroke="#e5b95f" stroke-width="3"/>
<text x="10" y="20" fill="#e5b95f" font-family="monospace" font-size="11" font-weight="900">LV 5</text>
<text x="10" y="39" fill="#f7f2d7" font-family="monospace" font-size="12" font-weight="900">Mend Renew</text>
</g>
<g transform="translate(154 46)">
<rect width="130" height="54" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="10" y="20" fill="#6da7df" font-family="monospace" font-size="11" font-weight="900">LV 10</text>
<text x="10" y="39" fill="#f7f2d7" font-family="monospace" font-size="12" font-weight="900">Rad Shield</text>
</g>
<g transform="translate(294 46)">
<rect width="130" height="54" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="10" y="20" fill="#4fb978" font-family="monospace" font-size="11" font-weight="900">LV 15</text>
<text x="10" y="39" fill="#f7f2d7" font-family="monospace" font-size="12" font-weight="900">Shield DR</text>
</g>
<g transform="translate(434 46)">
<rect width="130" height="54" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="10" y="20" fill="#b16dde" font-family="monospace" font-size="11" font-weight="900">LV 20</text>
<text x="10" y="39" fill="#f7f2d7" font-family="monospace" font-size="12" font-weight="900">Mend CD</text>
</g>
</g>
<g transform="translate(16 218)">
<rect width="278" height="286" fill="#191b25" stroke="#3a3944" stroke-width="2"/>
<text x="16" y="28" fill="#8aa0b7" font-family="monospace" font-size="10" font-weight="700">POOL</text>
<g transform="translate(14 46)">
<rect width="250" height="42" fill="#29291f" stroke="#e5b95f" stroke-width="3"/>
<text x="12" y="26" fill="#f5e6b2" font-family="monospace" font-size="13" font-weight="900">Mend applies Renew</text>
</g>
<g transform="translate(14 98)">
<rect width="250" height="42" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="12" y="26" fill="#f7f2d7" font-family="monospace" font-size="13" font-weight="900">Shield applies Renew</text>
</g>
<g transform="translate(14 150)">
<rect width="250" height="42" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="12" y="26" fill="#f7f2d7" font-family="monospace" font-size="13" font-weight="900">Mend adds Shield</text>
</g>
<g transform="translate(14 202)">
<rect width="250" height="42" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="12" y="26" fill="#f7f2d7" font-family="monospace" font-size="13" font-weight="900">Radiance Renew</text>
</g>
</g>
<g transform="translate(310 218)">
<rect width="294" height="286" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="16" y="28" fill="#8aa0b7" font-family="monospace" font-size="10" font-weight="700">DETAIL</text>
<text x="16" y="58" fill="#f5e6b2" font-family="monospace" font-size="18" font-weight="900">Mend applies Renew</text>
<text x="16" y="92" fill="#d7dbe0" font-family="monospace" font-size="13">Mend also applies Renew to</text>
<text x="16" y="112" fill="#d7dbe0" font-family="monospace" font-size="13">the target.</text>
<text x="16" y="150" fill="#83d99b" font-family="monospace" font-size="12" font-weight="900">Rule: same HoT refreshes.</text>
<text x="16" y="172" fill="#83d99b" font-family="monospace" font-size="12" font-weight="900">Different HoTs coexist.</text>
<rect x="16" y="216" width="120" height="44" rx="3" fill="#e5b95f" stroke="#0a0b0e" stroke-width="3"/>
<text x="50" y="244" fill="#21180a" font-family="monospace" font-size="14" font-weight="900">Equip</text>
<rect x="154" y="216" width="120" height="44" rx="3" fill="#191b25" stroke="#3a3944" stroke-width="2"/>
<text x="184" y="244" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Clear</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-label="Ayn Thor main display talent effect planner mockup">
<rect width="960" height="540" fill="#111219"/>
<rect x="20" y="18" width="920" height="50" fill="#20222d" stroke="#0a0b0e" stroke-width="3"/>
<text x="38" y="39" fill="#8aa0b7" font-family="monospace" font-size="12" font-weight="700">CHARACTER WORKSHOP</text>
<text x="38" y="59" fill="#f5e6b2" font-family="monospace" font-size="20" font-weight="900">Spell Effects</text>
<rect x="764" y="28" width="154" height="30" rx="3" fill="#e5b95f" stroke="#0a0b0e" stroke-width="3"/>
<text x="786" y="49" fill="#21180a" font-family="monospace" font-size="13" font-weight="900">Save Loadout</text>
<g transform="translate(20 88)">
<rect width="294" height="420" fill="#191b25" stroke="#3a3944" stroke-width="2"/>
<text x="18" y="28" fill="#8aa0b7" font-family="monospace" font-size="11" font-weight="700">UNLOCKED SLOTS</text>
<text x="18" y="53" fill="#f7f2d7" font-family="monospace" font-size="19" font-weight="900">4 active effects</text>
<g transform="translate(16 76)">
<rect width="262" height="58" fill="#29291f" stroke="#e5b95f" stroke-width="3"/>
<circle cx="29" cy="29" r="15" fill="#e5b95f"/>
<text x="22" y="34" fill="#21180a" font-family="monospace" font-size="13" font-weight="900">5</text>
<text x="56" y="25" fill="#f5e6b2" font-family="monospace" font-size="15" font-weight="900">Mend applies Renew</text>
<text x="56" y="45" fill="#83d99b" font-family="monospace" font-size="11">Selected</text>
</g>
<g transform="translate(16 148)">
<rect width="262" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<circle cx="29" cy="29" r="15" fill="#6da7df"/>
<text x="18" y="34" fill="#08111c" font-family="monospace" font-size="12" font-weight="900">10</text>
<text x="56" y="25" fill="#f7f2d7" font-family="monospace" font-size="15" font-weight="900">Radiance adds shield</text>
<text x="56" y="45" fill="#8aa0b7" font-family="monospace" font-size="11">30 percent strength</text>
</g>
<g transform="translate(16 220)">
<rect width="262" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<circle cx="29" cy="29" r="15" fill="#4fb978"/>
<text x="18" y="34" fill="#071408" font-family="monospace" font-size="12" font-weight="900">15</text>
<text x="56" y="25" fill="#f7f2d7" font-family="monospace" font-size="15" font-weight="900">Shielded takes less</text>
<text x="56" y="45" fill="#8aa0b7" font-family="monospace" font-size="11">20 percent damage cut</text>
</g>
<g transform="translate(16 292)">
<rect width="262" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<circle cx="29" cy="29" r="15" fill="#b16dde"/>
<text x="18" y="34" fill="#15071c" font-family="monospace" font-size="12" font-weight="900">20</text>
<text x="56" y="25" fill="#f7f2d7" font-family="monospace" font-size="15" font-weight="900">Mend lowers Radiance</text>
<text x="56" y="45" fill="#8aa0b7" font-family="monospace" font-size="11">-2 sec cooldown</text>
</g>
</g>
<g transform="translate(334 88)">
<rect width="606" height="286" fill="#191b25" stroke="#3a3944" stroke-width="2"/>
<text x="18" y="28" fill="#8aa0b7" font-family="monospace" font-size="11" font-weight="700">EFFECT POOL</text>
<text x="18" y="53" fill="#f7f2d7" font-family="monospace" font-size="19" font-weight="900">Pick effects, swap anytime</text>
<g transform="translate(18 76)">
<rect width="276" height="58" fill="#29291f" stroke="#e5b95f" stroke-width="3"/>
<text x="14" y="24" fill="#f5e6b2" font-family="monospace" font-size="14" font-weight="900">Mend applies Renew</text>
<text x="14" y="45" fill="#b7c3d0" font-family="monospace" font-size="11">Direct heal also applies Renew.</text>
<text x="226" y="24" fill="#83d99b" font-family="monospace" font-size="10" font-weight="900">ON</text>
</g>
<g transform="translate(312 76)">
<rect width="276" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="14" y="24" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Shield applies Renew</text>
<text x="14" y="45" fill="#b7c3d0" font-family="monospace" font-size="11">Sun Ward adds a Renew effect.</text>
</g>
<g transform="translate(18 148)">
<rect width="276" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="14" y="24" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Mend adds Shield</text>
<text x="14" y="45" fill="#b7c3d0" font-family="monospace" font-size="11">50 percent shield strength.</text>
</g>
<g transform="translate(312 148)">
<rect width="276" height="58" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="14" y="24" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Radiance adds Shield</text>
<text x="14" y="45" fill="#b7c3d0" font-family="monospace" font-size="11">30 percent to affected allies.</text>
</g>
<g transform="translate(18 220)">
<rect width="276" height="46" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="14" y="20" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Radiance applies Renew</text>
<text x="14" y="38" fill="#b7c3d0" font-family="monospace" font-size="11">50 percent duration.</text>
</g>
<g transform="translate(312 220)">
<rect width="276" height="46" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="14" y="20" fill="#f7f2d7" font-family="monospace" font-size="14" font-weight="900">Shielded gets +healing</text>
<text x="14" y="38" fill="#b7c3d0" font-family="monospace" font-size="11">20 percent more healing.</text>
</g>
</g>
<g transform="translate(334 392)">
<rect width="606" height="116" fill="#20222d" stroke="#3a3944" stroke-width="2"/>
<text x="18" y="26" fill="#8aa0b7" font-family="monospace" font-size="11" font-weight="700">SELECTED EFFECT</text>
<text x="18" y="52" fill="#f5e6b2" font-family="monospace" font-size="20" font-weight="900">Mend applies Renew</text>
<text x="18" y="78" fill="#d7dbe0" font-family="monospace" font-size="13">Casting Mend also applies Renew to the same target. Renew refreshes itself; different HoTs coexist.</text>
<rect x="470" y="32" width="112" height="42" rx="3" fill="#e5b95f" stroke="#0a0b0e" stroke-width="3"/>
<text x="497" y="58" fill="#21180a" font-family="monospace" font-size="14" font-weight="900">Equip</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

+5 -2
View File
@@ -6,16 +6,19 @@
"scripts": {
"predev": "npm run db:init",
"dev": "vite",
"build": "npx tsc -b --noEmit && npx vite build && node scripts/generate-service-worker.mjs",
"build": "npm run offline:export && npx tsc -b --noEmit && npx vite build && node scripts/generate-service-worker.mjs",
"android:sync": "npm run build && cap sync android",
"android:sync:truenas": "VITE_API_BASE_URL=https://iwanttoheal.phenomrom.com npm run android:sync",
"android:open": "cap open android",
"android:apk": "npm run android:sync && cd android && ./gradlew assembleDebug",
"android:apk": "npm run android:sync && cd android && ./gradlew clean assembleDebug",
"android:apk:truenas": "VITE_API_BASE_URL=https://iwanttoheal.phenomrom.com npm run android:apk",
"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",
"auth:start": "node server/auth.mjs",
"start": "node server/production.mjs",
"prepreview": "npm run db:init",
"preview": "vite preview"
+8 -1
View File
@@ -1,4 +1,4 @@
import { mkdirSync } from 'node:fs'
import { existsSync, mkdirSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { DatabaseSync } from 'node:sqlite'
@@ -7,6 +7,7 @@ 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')
const adminOverridesUrl = new URL('../db/admin-overrides.sql', import.meta.url)
database.exec(schema)
@@ -205,6 +206,7 @@ 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('dungeons', 'image_url', "TEXT NOT NULL DEFAULT '/boss-placeholder.svg'")
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 '+'")
@@ -249,6 +251,11 @@ addColumnIfMissing('items', 'image_url', "TEXT NOT NULL DEFAULT '/equipment-plac
database.exec(seed)
if (existsSync(adminOverridesUrl)) {
const adminOverrides = await readFile(adminOverridesUrl, 'utf8')
database.exec(adminOverrides)
}
const counts = database
.prepare(`
SELECT
+283 -7
View File
@@ -9,8 +9,10 @@ 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 adminOverridesPath = fileURLToPath(new URL('../db/admin-overrides.sql', 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 dungeonImageDirectory = fileURLToPath(new URL('../data/uploads/dungeons/', import.meta.url))
const bossImageContentTypes = {
'.gif': 'image/gif',
@@ -26,6 +28,88 @@ function sendJson(response, status, body) {
response.end(JSON.stringify(body))
}
function sqlValue(value) {
if (value === null || value === undefined) return 'NULL'
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL'
return `'${String(value).replaceAll("'", "''")}'`
}
function writeAdminOverrides(database) {
const lines = [
'-- Generated by local admin panel. Commit this file with uploaded art changes.',
'PRAGMA foreign_keys = ON;',
'BEGIN TRANSACTION;',
'',
]
for (const dungeon of database.prepare(`
SELECT id, slug, name, recommended_level AS recommendedLevel,
content_type AS contentType, party_size AS partySize,
experience_reward AS experienceReward, image_url AS imageUrl, description
FROM dungeons ORDER BY id
`).all()) {
lines.push(`UPDATE dungeons SET slug = ${sqlValue(dungeon.slug)}, name = ${sqlValue(dungeon.name)}, recommended_level = ${sqlValue(dungeon.recommendedLevel)}, content_type = ${sqlValue(dungeon.contentType)}, party_size = ${sqlValue(dungeon.partySize)}, experience_reward = ${sqlValue(dungeon.experienceReward)}, image_url = ${sqlValue(dungeon.imageUrl)}, description = ${sqlValue(dungeon.description)} WHERE id = ${sqlValue(dungeon.id)};`)
}
lines.push('')
for (const encounter of database.prepare(`
SELECT id, slug, name, encounter_type AS encounterType,
max_health AS maxHealth, base_damage AS baseDamage,
tank_damage AS tankDamage, party_damage AS partyDamage,
description, image_url AS imageUrl
FROM encounters ORDER BY id
`).all()) {
lines.push(`UPDATE encounters SET slug = ${sqlValue(encounter.slug)}, name = ${sqlValue(encounter.name)}, encounter_type = ${sqlValue(encounter.encounterType)}, max_health = ${sqlValue(encounter.maxHealth)}, base_damage = ${sqlValue(encounter.baseDamage)}, tank_damage = ${sqlValue(encounter.tankDamage)}, party_damage = ${sqlValue(encounter.partyDamage)}, description = ${sqlValue(encounter.description)}, image_url = ${sqlValue(encounter.imageUrl)} WHERE id = ${sqlValue(encounter.id)};`)
}
lines.push('')
for (const item of 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 id
`).all()) {
lines.push(`UPDATE items SET slug = ${sqlValue(item.slug)}, name = ${sqlValue(item.name)}, slot = ${sqlValue(item.slot)}, rarity = ${sqlValue(item.rarity)}, item_level = ${sqlValue(item.itemLevel)}, healing_power = ${sqlValue(item.healingPower)}, max_resource_bonus = ${sqlValue(item.maxResourceBonus)}, glyph = ${sqlValue(item.glyph)}, image_url = ${sqlValue(item.imageUrl)}, description = ${sqlValue(item.description)} WHERE id = ${sqlValue(item.id)};`)
}
lines.push('', 'DELETE FROM encounter_loot;')
for (const loot of 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()) {
lines.push(`INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (${sqlValue(loot.encounterId)}, ${sqlValue(loot.itemId)}, ${sqlValue(loot.difficultyId)}, ${sqlValue(loot.dropWeight)}, ${sqlValue(loot.dropChance)});`)
}
lines.push('')
for (const recipe of database.prepare(`
SELECT id, difficulty_id AS difficultyId,
source_dungeon_id AS sourceDungeonId, source_encounter_id AS sourceEncounterId
FROM crafting_recipes ORDER BY id
`).all()) {
lines.push(`UPDATE crafting_recipes SET difficulty_id = ${sqlValue(recipe.difficultyId)}, source_dungeon_id = ${sqlValue(recipe.sourceDungeonId)}, source_encounter_id = ${sqlValue(recipe.sourceEncounterId)} WHERE id = ${sqlValue(recipe.id)};`)
}
lines.push('', 'DELETE FROM crafting_recipe_components;')
for (const component of database.prepare(`
SELECT recipe_id AS recipeId, item_id AS itemId, quantity
FROM crafting_recipe_components ORDER BY recipe_id, item_id
`).all()) {
lines.push(`INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (${sqlValue(component.recipeId)}, ${sqlValue(component.itemId)}, ${sqlValue(component.quantity)});`)
}
lines.push('', 'DELETE FROM gear_upgrade_paths;')
for (const path of database.prepare(`
SELECT from_item_id AS fromItemId, to_item_id AS toItemId
FROM gear_upgrade_paths ORDER BY from_item_id
`).all()) {
lines.push(`INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) VALUES (${sqlValue(path.fromItemId)}, ${sqlValue(path.toItemId)});`)
}
lines.push('', 'COMMIT;', '')
writeFileSync(adminOverridesPath, lines.join('\n'), { mode: 0o644 })
}
async function readJson(request, maxSize = 16 * 1024) {
const chunks = []
let size = 0
@@ -83,14 +167,33 @@ function sendItemImage(request, response) {
createReadStream(imagePath).pipe(response)
}
function sendDungeonImage(request, response) {
const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname)
const filename = pathname.replace('/api/dungeon-images/', '')
if (!/^[A-Za-z0-9._-]+$/.test(filename)) {
sendJson(response, 404, { error: 'Image not found.' })
return
}
const imagePath = resolve(dungeonImageDirectory, filename)
const insideDirectory = imagePath.startsWith(resolve(dungeonImageDirectory) + 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.')
}
if (!encounter) throw new Error('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.')
@@ -126,6 +229,25 @@ function saveItemImage(database, itemId, payload) {
return imageUrl
}
function saveDungeonImage(database, dungeonId, payload) {
const dungeon = database.prepare(`SELECT id, slug FROM dungeons WHERE id = ?`).get(dungeonId)
if (!dungeon) throw new Error('Dungeon 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('Dungeon image must be 1 byte to 4 MB.')
}
mkdirSync(dungeonImageDirectory, { recursive: true })
const filename = `${dungeon.slug}-${Date.now()}-${randomBytes(4).toString('hex')}.${extensionByType[match[1]]}`
writeFileSync(resolve(dungeonImageDirectory, filename), bytes, { mode: 0o644 })
const imageUrl = `/api/dungeon-images/${filename}`
database.prepare(`UPDATE dungeons SET image_url = ? WHERE id = ?`).run(imageUrl, dungeonId)
return imageUrl
}
function sendFile(response, filePath) {
const contentTypes = {
'.css': 'text/css; charset=utf-8',
@@ -181,6 +303,11 @@ const server = createServer(async (request, response) => {
return
}
if (request.url.startsWith('/api/dungeon-images/') && request.method === 'GET') {
sendDungeonImage(request, response)
return
}
if (!existsSync(databasePath)) {
sendJson(response, 503, { error: 'Database missing. Run npm run db:init.' })
return
@@ -201,7 +328,9 @@ const server = createServer(async (request, response) => {
`).all()
const encounters = database.prepare(`
SELECT id, dungeon_id AS dungeonId, sequence, slug, name AS enemyName,
encounter_type AS encounterType, image_url AS imageUrl
encounter_type AS encounterType, max_health AS maxHealth, base_damage AS baseDamage,
tank_damage AS tankDamage, party_damage AS partyDamage,
description, image_url AS imageUrl
FROM encounters ORDER BY dungeon_id, sequence
`).all()
const difficulties = database.prepare(`
@@ -235,8 +364,56 @@ const server = createServer(async (request, response) => {
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 })
const dungeons = database.prepare(`
SELECT id, slug, name, recommended_level AS recommendedLevel,
content_type AS contentType, party_size AS partySize,
experience_reward AS experienceReward, image_url AS imageUrl,
description
FROM dungeons ORDER BY id
`).all()
const gearUpgradePaths = database.prepare(`
SELECT from_item_id AS fromItemId, to_item_id AS toItemId
FROM gear_upgrade_paths ORDER BY from_item_id
`).all()
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 abilities = 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 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
FROM talents
LEFT JOIN talents AS prerequisite
ON prerequisite.id = talents.prerequisite_talent_id
ORDER BY talents.class_id, talents.tier, talents.branch
`).all()
sendJson(response, 200, {
items,
encounters,
difficulties,
encounterLoot,
craftingRecipes: [...recipes.values()],
dungeons,
gearUpgradePaths,
classes: classes.map((gameClass) => ({
...gameClass,
abilities: abilities.filter((ability) => ability.classId === gameClass.id),
talents: talents.filter((talent) => talent.classId === gameClass.id),
})),
})
return
}
@@ -244,6 +421,7 @@ const server = createServer(async (request, response) => {
if (bossImageMatch && request.method === 'PUT') {
const payload = await readJson(request, 6 * 1024 * 1024)
const imageUrl = saveBossImage(database, Number(bossImageMatch[1]), payload)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true, imageUrl })
return
}
@@ -252,6 +430,16 @@ const server = createServer(async (request, response) => {
if (itemImageMatch && request.method === 'PUT') {
const payload = await readJson(request, 6 * 1024 * 1024)
const imageUrl = saveItemImage(database, Number(itemImageMatch[1]), payload)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true, imageUrl })
return
}
const dungeonImageMatch = request.url.match(/^\/api\/admin\/dungeons\/(\d+)\/image$/)
if (dungeonImageMatch && request.method === 'PUT') {
const payload = await readJson(request, 6 * 1024 * 1024)
const imageUrl = saveDungeonImage(database, Number(dungeonImageMatch[1]), payload)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true, imageUrl })
return
}
@@ -271,6 +459,47 @@ const server = createServer(async (request, response) => {
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)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
const dungeonMatch = request.url.match(/^\/api\/admin\/dungeons\/(\d+)$/)
if (dungeonMatch && request.method === 'PUT') {
const payload = await readJson(request)
const dungeonId = Number(dungeonMatch[1])
const fields = []
const values = []
for (const key of ['name', 'slug', 'content_type', 'description']) {
if (payload[key] !== undefined) { fields.push(`${key} = ?`); values.push(payload[key]) }
}
for (const key of ['recommended_level', 'party_size', 'experience_reward']) {
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(dungeonId)
database.prepare(`UPDATE dungeons SET ${fields.join(', ')} WHERE id = ?`).run(...values)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
const encounterMatch = request.url.match(/^\/api\/admin\/encounters\/(\d+)$/)
if (encounterMatch && request.method === 'PUT') {
const payload = await readJson(request)
const encounterId = Number(encounterMatch[1])
const fields = []
const values = []
for (const key of ['name', 'slug', 'encounter_type', 'description']) {
if (payload[key] !== undefined) { fields.push(`${key} = ?`); values.push(payload[key]) }
}
for (const key of ['max_health', 'base_damage', 'tank_damage', 'party_damage']) {
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(encounterId)
database.prepare(`UPDATE encounters SET ${fields.join(', ')} WHERE id = ?`).run(...values)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
@@ -283,6 +512,7 @@ const server = createServer(async (request, response) => {
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)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
@@ -291,6 +521,7 @@ const server = createServer(async (request, response) => {
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]))
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
@@ -298,12 +529,33 @@ const server = createServer(async (request, response) => {
const recipeComponents = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components$/)
if (recipeComponents && request.method === 'POST') {
const payload = await readJson(request)
const quantity = Number(payload.quantity)
if (!Number.isInteger(quantity) || quantity < 1) throw new Error('Component quantity must be at least 1.')
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)
`).run(Number(recipeComponents[1]), payload.itemId, quantity)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
const recipeMatch = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)$/)
if (recipeMatch && request.method === 'PUT') {
const payload = await readJson(request)
database.prepare(`
UPDATE crafting_recipes
SET difficulty_id = ?, source_dungeon_id = ?, source_encounter_id = ?
WHERE id = ?
`).run(
payload.difficultyId ? Number(payload.difficultyId) : null,
payload.sourceDungeonId ? Number(payload.sourceDungeonId) : null,
payload.sourceEncounterId ? Number(payload.sourceEncounterId) : null,
Number(recipeMatch[1]),
)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
@@ -312,6 +564,30 @@ const server = createServer(async (request, response) => {
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]))
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
if (request.url === '/api/admin/gear-upgrade-paths' && request.method === 'POST') {
const payload = await readJson(request)
const fromItemId = Number(payload.fromItemId)
const toItemId = Number(payload.toItemId)
if (!fromItemId || !toItemId || fromItemId === toItemId) throw new Error('Choose two different equipment items.')
database.prepare(`
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id)
VALUES (?, ?)
ON CONFLICT(from_item_id) DO UPDATE SET to_item_id = excluded.to_item_id
`).run(fromItemId, toItemId)
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
const upgradePathDelete = request.url.match(/^\/api\/admin\/gear-upgrade-paths\/(\d+)$/)
if (upgradePathDelete && request.method === 'DELETE') {
database.prepare(`DELETE FROM gear_upgrade_paths WHERE from_item_id = ?`).run(Number(upgradePathDelete[1]))
writeAdminOverrides(database)
sendJson(response, 200, { ok: true })
return
}
+18
View File
@@ -0,0 +1,18 @@
import { createServer } from 'node:http'
import { handleAuthApiRequest } from './game-api.mjs'
process.env.NODE_ENV = process.env.NODE_ENV ?? 'production'
process.env.CORS_ORIGINS = process.env.CORS_ORIGINS
?? process.env.AUTH_CORS_ORIGINS
?? '*'
const host = process.env.AUTH_HOST ?? process.env.HOST ?? '127.0.0.1'
const port = Number(process.env.AUTH_PORT ?? process.env.PORT ?? 4174)
const server = createServer((request, response) => {
handleAuthApiRequest(request, response)
})
server.listen(port, host, () => {
console.log(`I want to Heal auth listening on http://${host}:${port}`)
})
+838 -83
View File
File diff suppressed because it is too large Load Diff
+3694 -39
View File
File diff suppressed because it is too large Load Diff
+416 -208
View File
@@ -19,7 +19,12 @@ import {
type AuthSession,
type CharacterProfile,
} from './profile'
import { getGameMode, type GameMode } from './gameRepository'
import {
getCloudSyncStatus,
getGameMode,
syncCloudSave,
type GameMode,
} from './gameRepository'
import { focusFirstControl } from './input.tsx'
type Screen =
@@ -40,8 +45,8 @@ const MENU_ITEMS: Array<{
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: 'dungeons', label: 'Dungeons', glyph: 'D', description: 'Guide a six-player party through dangerous encounters.' },
{ screen: 'raids', label: 'Raids', glyph: 'R', description: 'Guide an eighteen-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.' },
@@ -49,6 +54,8 @@ const MENU_ITEMS: Array<{
]
const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty'
const SHOW_LEADERBOARDS = false
const ACTIVITY_PAGE_SIZE = 4
function activityInitials(name: string) {
return name
@@ -75,19 +82,22 @@ function App() {
return Number.isFinite(saved) && saved > 0 ? saved : 1
})
const [selectedDungeonId, setSelectedDungeonId] = useState(1)
const [selectedRaidId, setSelectedRaidId] = useState(2)
const [selectedRaidId, setSelectedRaidId] = useState(20)
const [roguelikeKind, setRoguelikeKind] = useState<'dungeon' | 'raid'>('dungeon')
const [roguelikeVariant, setRoguelikeVariant] = useState<RoguelikeVariant>('pve')
const [roguelikeUpgradeTiming, setRoguelikeUpgradeTiming] = useState<RoguelikeUpgradeTiming>('encounter')
const [roguelikeAbilityLabelMode, setRoguelikeAbilityLabelMode] = useState<RoguelikeAbilityLabelMode>('ability')
const [pvpContentType, setPvpContentType] = useState<PvpContentType>('dungeon')
const [selectedPart, setSelectedPart] = useState(1)
const [selectedMarathonMode, setSelectedMarathonMode] = useState(false)
const [activityPage, setActivityPage] = useState(0)
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('')
const [syncingCloud, setSyncingCloud] = useState(false)
const [syncMessage, setSyncMessage] = useState('')
useEffect(() => {
loadAuthSession()
@@ -105,6 +115,17 @@ function App() {
.finally(() => setAuthChecked(true))
}, [])
useEffect(() => {
const handleModeChange = (event: Event) => {
const nextMode = (event as CustomEvent<GameMode>).detail
setGameMode(nextMode)
}
window.addEventListener('chronicle:mode-changed', handleModeChange as EventListener)
return () => {
window.removeEventListener('chronicle:mode-changed', handleModeChange as EventListener)
}
}, [])
useEffect(() => {
if (screen === 'combat') return
window.requestAnimationFrame(() => {
@@ -112,6 +133,13 @@ function App() {
})
}, [screen])
useEffect(() => {
if (!authChecked || !account || !profile || screen === 'combat') return
window.requestAnimationFrame(() => {
focusFirstControl()
})
}, [account, authChecked, profile, screen])
useEffect(() => {
window.localStorage.setItem(LAST_DIFFICULTY_KEY, String(selectedDifficultyId))
}, [selectedDifficultyId])
@@ -129,6 +157,9 @@ function App() {
setScreen('menu')
setError('')
setServerMessage('')
window.requestAnimationFrame(() => {
focusFirstControl()
})
}
async function signOut() {
@@ -138,11 +169,27 @@ function App() {
setProfile(null)
setGameMode(getGameMode())
setScreen('menu')
setSyncMessage('')
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Unable to sign out.')
}
}
async function syncSaveNow() {
setSyncingCloud(true)
setSyncMessage('')
try {
const updated = await syncCloudSave()
setProfile(updated)
setGameMode(getGameMode())
setSyncMessage('Cloud save updated.')
} catch (reason) {
setSyncMessage(reason instanceof Error ? reason.message : 'Unable to sync cloud save.')
} finally {
setSyncingCloud(false)
}
}
if (error) {
return (
<main className="game-shell">
@@ -185,17 +232,18 @@ function App() {
const roguelikePool = profile.dungeons
.filter((candidate) => candidate.contentType === roguelikeKind)
.flatMap((candidate) => candidate.encounters)
const startPart = selectedPart
return (
<CombatScreen
difficulty={difficulty}
dungeon={dungeon}
hardMode={false}
marathonMode={selectedMarathonMode && combatContentId > 0}
profile={profile}
roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined}
roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined}
roguelikeAbilityLabelMode={combatContentId < 0 ? roguelikeAbilityLabelMode : undefined}
roguelikeEncounterPool={combatContentId < 0 ? roguelikePool : undefined}
startPart={startPart}
startPart={1}
onExit={() => {
setScreen(combatContentId < 0 ? 'roguelike' : dungeon.contentType === 'raid' ? 'raids' : 'dungeons')
}}
@@ -237,22 +285,56 @@ function App() {
?? 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 startPveRoguelike = () => {
const baseDungeon = dungeonOptions[0]
const baseRaid = raidOptions[0]
if (roguelikeKind === 'raid') {
setCombatContentId(-2)
setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101)
} else {
setCombatContentId(-1)
setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1)
}
setSelectedMarathonMode(false)
setScreen('combat')
}
const tierOptions = activityOptions
.flatMap((option) => option.difficulties)
.filter((difficulty, index, all) => (
all.findIndex((candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel) === index
))
.sort((a, b) => a.droppedItemLevel - b.droppedItemLevel)
const savedDifficulty = profile.dungeons
.flatMap((option) => option.difficulties)
.find((candidate) => candidate.id === selectedDifficultyId)
const selectedTier = tierOptions.find((candidate) => (
candidate.droppedItemLevel === savedDifficulty?.droppedItemLevel
&& profile.character.level >= candidate.unlockLevel
))
?? tierOptions.slice().reverse().find((candidate) => profile.character.level >= candidate.unlockLevel)
?? tierOptions[0]
const selectedTierItemLevel = selectedTier?.droppedItemLevel ?? 0
const activityPageCount = Math.max(1, Math.ceil(activityOptions.length / ACTIVITY_PAGE_SIZE))
const currentActivityPage = Math.min(activityPage, activityPageCount - 1)
const pagedActivityOptions = activityOptions.slice(
currentActivityPage * ACTIVITY_PAGE_SIZE,
currentActivityPage * ACTIVITY_PAGE_SIZE + ACTIVITY_PAGE_SIZE,
)
const activityPageStart = activityOptions.length === 0
? 0
: currentActivityPage * ACTIVITY_PAGE_SIZE + 1
const activityPageEnd = Math.min(activityOptions.length, (currentActivityPage + 1) * ACTIVITY_PAGE_SIZE)
const selectedActivityId = screen === 'raids' && raid ? raid.id : dungeon.id
const activity = activityOptions.find((candidate) => candidate.id === selectedActivityId)
?? activityOptions[0]
?? (screen === 'raids' && raid ? raid : dungeon)
const selectedDifficulty = activity.difficulties.find(
(candidate) => candidate.id === selectedDifficultyId,
(candidate) => candidate.droppedItemLevel === selectedTierItemLevel,
) ?? 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 cloudSync = getCloudSyncStatus()
const canShowCloudSync = account.id !== -1 && cloudSync.available
const lootPreviewEncounters = [...activity.encounters]
.filter((encounter) => encounter.isBoss)
.sort((a, b) => lootSort === 'boss'
@@ -260,7 +342,7 @@ function App() {
: a.sequence - b.sequence)
return (
<main className="game-shell">
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}>
<header className="topbar app-header">
<button
className="brand-button"
@@ -285,6 +367,28 @@ function App() {
{screen === 'menu' && (
<section className="menu-screen">
<div className="main-menu-grid">
{canShowCloudSync && (
<div className="menu-card cloud-sync-card">
<span>{cloudSync.dirty ? 'S' : 'C'}</span>
<div>
<strong>Cloud Save</strong>
<small>
{cloudSync.dirty
? 'Local progress waiting. Upload when you want to refresh the server copy.'
: 'Server copy matches this device.'}
</small>
{syncMessage && <small className="cloud-sync-message">{syncMessage}</small>}
</div>
<button
className="text-button"
disabled={syncingCloud || !cloudSync.dirty}
onClick={syncSaveNow}
type="button"
>
{syncingCloud ? 'Syncing...' : cloudSync.dirty ? 'Sync Save To Server' : 'Already Synced'}
</button>
</div>
)}
{MENU_ITEMS.map((item) => (
<button
className="menu-card"
@@ -335,84 +439,90 @@ function App() {
</div>
{roguelikeVariant === 'pve' && (
<>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Timing</p>
<h2>Buff Drafts</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeUpgradeTiming === 'encounter' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('encounter')}
type="button"
>
Every Encounter
</button>
<button
className={`text-button ${roguelikeUpgradeTiming === 'boss' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('boss')}
type="button"
>
Boss Only
</button>
</div>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Labels</p>
<h2>Display Mode</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeAbilityLabelMode === 'ability' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('ability')}
type="button"
>
Ability Names
</button>
<button
className={`text-button ${roguelikeAbilityLabelMode === 'slot' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('slot')}
type="button"
>
Slot Names
</button>
</div>
</div>
<div className="roguelike-mode-grid">
<button
className="menu-card"
onClick={() => {
const baseDungeon = dungeonOptions[0]
setRoguelikeKind('dungeon')
setCombatContentId(-1)
setSelectedDifficultyId(baseDungeon.difficulties[0]?.id ?? 1)
setSelectedPart(1)
setScreen('combat')
}}
type="button"
>
<span>D</span>
<strong>Dungeon Roguelike</strong>
<small>Five-player party. Two random trash enemies and a boss with a lighter early ramp.</small>
</button>
<button
className="menu-card"
onClick={() => {
const baseRaid = raidOptions[0]
setRoguelikeKind('raid')
setCombatContentId(-2)
setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101)
setSelectedPart(1)
setScreen('combat')
}}
type="button"
>
<span>R</span>
<strong>Raid Roguelike</strong>
<small>Ten-player party. Raid pools, lighter early scaling, and the same upgrade draft.</small>
</button>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Run Type</p>
<h2>PvE Roguelike</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeKind === 'dungeon' ? 'active' : ''}`}
onClick={() => setRoguelikeKind('dungeon')}
type="button"
>
Dungeon
</button>
<button
className={`text-button ${roguelikeKind === 'raid' ? 'active' : ''}`}
onClick={() => setRoguelikeKind('raid')}
type="button"
>
Raid
</button>
</div>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Timing</p>
<h2>Buff Drafts</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeUpgradeTiming === 'encounter' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('encounter')}
type="button"
>
Every Encounter
</button>
<button
className={`text-button ${roguelikeUpgradeTiming === 'boss' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('boss')}
type="button"
>
Boss Only
</button>
</div>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Labels</p>
<h2>Display Mode</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeAbilityLabelMode === 'ability' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('ability')}
type="button"
>
Ability Names
</button>
<button
className={`text-button ${roguelikeAbilityLabelMode === 'slot' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('slot')}
type="button"
>
Slot Names
</button>
</div>
</div>
<div className="menu-card pvp-queue-panel">
<span>{roguelikeKind === 'raid' ? 'R' : 'D'}</span>
<div>
<strong>{roguelikeKind === 'raid' ? 'Raid Roguelike' : 'Dungeon Roguelike'}</strong>
<small>
{roguelikeKind === 'raid'
? 'Ten-player party. Raid pools, lighter early scaling, and the same upgrade draft.'
: 'Five-player party. Two random trash enemies and a boss with a lighter early ramp.'}
</small>
</div>
<button
className="text-button"
onClick={startPveRoguelike}
type="button"
>
Start Run
</button>
</div>
</>
)}
{roguelikeVariant === 'pvp' && (
@@ -457,6 +567,7 @@ function App() {
Start Match
</button>
</div>
{SHOW_LEADERBOARDS && (
<div className="leaderboard-section">
<div className="equipment-heading toggle-heading">
<div>
@@ -488,114 +599,203 @@ function App() {
)}
</div>
</div>
)}
</>
)}
</section>
)}
{(screen === 'dungeons' || screen === 'raids') && (
<section className="content-screen">
<ScreenHeading
eyebrow="Adventure"
title={activity.contentType === 'raid' ? 'Raids' : 'Dungeons'}
onBack={() => setScreen('menu')}
/>
<article className="dungeon-card">
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(activity.name)}
<section className="content-screen dungeon-run-screen">
<div className="dungeon-run-board">
<div className="dungeon-run-main">
<article className="run-summary-card dungeon-focus-card">
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(activity.name)}
</div>
<div className="run-summary-copy">
<p className="eyebrow">Selected Run</p>
<div className="run-title-row">
<h2>{activity.name}</h2>
<button className="back-button inline-back-button" onClick={() => setScreen('menu')} type="button">Back</button>
</div>
<p>{activity.description}</p>
<div className="tag-row">
<span>Level {activity.recommendedLevel}</span>
<span>{activity.partySize} Players</span>
<span>{selectedDifficulty.name}</span>
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
</div>
</div>
</article>
<section className="run-setup-panel dungeon-choice-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Pick Run</p>
<h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
</div>
{activityPageCount > 1 ? (
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
<button
disabled={currentActivityPage === 0}
onClick={() => setActivityPage((page) => Math.max(0, page - 1))}
type="button"
>
Prev
</button>
<span>{activityPageStart}-{activityPageEnd} of {activityOptions.length}</span>
<button
disabled={currentActivityPage >= activityPageCount - 1}
onClick={() => setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))}
type="button"
>
Next
</button>
</div>
) : (
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
)}
</div>
<div className="activity-card-grid dungeon-choice-grid">
{pagedActivityOptions.map((candidate) => {
const difficulty = candidate.difficulties.find(
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
) ?? candidate.difficulties[0]
const locked = profile.character.level < difficulty.unlockLevel
const selected = candidate.id === activity.id
return (
<button
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
disabled={locked}
key={candidate.id}
onClick={() => {
if (screen === 'raids') setSelectedRaidId(candidate.id)
else setSelectedDungeonId(candidate.id)
setSelectedDifficultyId(difficulty.id)
}}
type="button"
>
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(candidate.name)}
</span>
<strong>{candidate.name}</strong>
<small>{candidate.locationName}</small>
<i>
Level {candidate.recommendedLevel} | {candidate.partySize} Players
</i>
</button>
)
})}
</div>
</section>
</div>
<div>
<p className="eyebrow">{activity.locationName}</p>
<h2>{activity.name}</h2>
<p>{activity.description}</p>
<div className="tag-row">
<span>Level {activity.recommendedLevel}</span>
<span>{activity.partySize} Players</span>
<span>{selectedDifficulty.name}</span>
<span>Component Level {selectedDifficulty.droppedItemLevel}</span>
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
<aside className="dungeon-setup-rail">
<section className="run-setup-panel tier-setup-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Item Level</p>
<h2>Tier</h2>
</div>
<small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by level.</small>
</div>
<div className="tier-grid">
{tierOptions.map((difficulty) => {
const locked = profile.character.level < difficulty.unlockLevel
const selected = difficulty.droppedItemLevel === selectedDifficulty.droppedItemLevel
return (
<button
className={`${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
disabled={locked}
key={difficulty.id}
onClick={() => {
setActivityPage(0)
const nextActivity = activity.difficulties.some(
(candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel,
)
? activity
: activityOptions.find((option) =>
option.difficulties.some((candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel),
)
if (nextActivity) {
if (screen === 'raids') setSelectedRaidId(nextActivity.id)
else setSelectedDungeonId(nextActivity.id)
const nextDifficulty = nextActivity.difficulties.find(
(candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel,
)
if (nextDifficulty) setSelectedDifficultyId(nextDifficulty.id)
}
}}
type="button"
>
<strong>iLvl {difficulty.droppedItemLevel}</strong>
<span>{locked ? `Level ${difficulty.unlockLevel}` : difficulty.name}</span>
</button>
)
})}
</div>
</section>
<section className="run-setup-panel part-setup-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Start</p>
<h2>Run</h2>
</div>
<small>
{difficultyLocked
? `Unlocks at level ${selectedDifficulty.unlockLevel}`
: 'Marathon keeps health and mana between boss kills.'}
</small>
</div>
<div className="part-picker">
<button
className="primary-button selected-part"
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(false)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
type="button"
>
Start Hunt
</button>
<button
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''}`}
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(true)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
type="button"
>
Marathon
</button>
</div>
</section>
<div className="difficulty-section compact-difficulty-section">
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
<div>
<strong>{selectedDifficulty.name}</strong>
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : selectedDifficulty.description}</small>
</div>
<dl>
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
<div><dt>Loot</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
</dl>
</div>
</div>
</div>
{activityOptions.length > 1 && (
<label className="activity-select">
<span>{activity.contentType === 'raid' ? 'Raid' : 'Dungeon'}</span>
<select
value={activity.id}
onChange={(event) => {
const nextActivityId = Number(event.target.value)
const nextActivity = activityOptions.find((candidate) => candidate.id === nextActivityId)
if (screen === 'raids') setSelectedRaidId(nextActivityId)
else setSelectedDungeonId(nextActivityId)
if (nextActivity?.difficulties[0]) {
setSelectedDifficultyId(nextActivity.difficulties[0].id)
}
}}
>
{activityOptions.map((candidate) => (
<option key={candidate.id} value={candidate.id}>{candidate.name}</option>
))}
</select>
</label>
)}
<div className="part-buttons">
{parts.map((p) => (
<button
key={p.part}
className={`primary-button ${selectedPart === p.part ? 'selected-part' : ''} ${!p.unlocked ? 'locked' : ''}`}
disabled={difficultyLocked || !p.unlocked}
onClick={() => {
setSelectedPart(p.part)
setCombatContentId(activity.id)
setScreen('combat')
}}
type="button"
>
{p.name}
</button>
))}
</div>
</article>
<div className="difficulty-section compact-difficulty-section">
<div className="difficulty-select-row">
<div>
<p className="eyebrow">Challenge Tier</p>
<h2>Difficulty</h2>
</div>
<label>
<span>Select</span>
<select
value={selectedDifficulty.id}
onChange={(event) => setSelectedDifficultyId(Number(event.target.value))}
>
{activity.difficulties.map((difficulty, index) => (
<option
disabled={profile.character.level < difficulty.unlockLevel}
key={difficulty.id}
value={difficulty.id}
>
{index + 1}. {difficulty.name}
{profile.character.level < difficulty.unlockLevel
? ` - Level ${difficulty.unlockLevel}`
: ''}
</option>
))}
</select>
</label>
</div>
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
<div>
<strong>{selectedDifficulty.name}</strong>
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : selectedDifficulty.description}</small>
</div>
<dl>
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
<div><dt>Components</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
</dl>
</div>
</div>
<div className="loot-preview-section">
<div className="loot-preview-section">
<div className="equipment-heading toggle-heading">
<div>
<p className="eyebrow">Encounter Rewards</p>
@@ -621,7 +821,7 @@ function App() {
</label>
</div>
<p className="section-note">
Bosses roll {lootChanceSlots} loot chances against the listed table, 1-3 materials each
Bosses drop 1-3 boss coins from one loot roll
{activity.completionItemLevel
? ` - full clear guarantees iLvl ${activity.completionItemLevel}`
: ''}
@@ -641,7 +841,7 @@ function App() {
)}
<div>
<strong>{encounter.enemyName}</strong>
<small>{loot.length > 0 ? `${lootChanceSlots} weighted chances, 1-3 each` : 'No component table'}</small>
<small>{loot.length > 0 ? '1 loot roll, 1-3 coins' : 'No coin table'}</small>
</div>
</div>
<div className="loot-items">
@@ -662,8 +862,9 @@ function App() {
</div>
</>
)}
</div>
<div className="leaderboard-section">
</div>
{SHOW_LEADERBOARDS && (
<div className="leaderboard-section">
<div className="equipment-heading toggle-heading">
<div>
<p className="eyebrow">Efficiency Rankings</p>
@@ -682,14 +883,16 @@ function App() {
<p className="section-note">
{gameMode === 'offline'
? 'Offline runs are not submitted'
: 'Lowest resource spent ranks first'}
: canShowCloudSync
? 'Manual save sync updates your cloud profile.'
: 'Lowest resource spent ranks first'}
</p>
<div className="leaderboard-tabs">
{([
{ key: 'part_1', label: `${sectionName} 1` },
{ key: 'part_2', label: `${sectionName} 2` },
{ key: 'part_3', label: `${sectionName} 3` },
{ key: 'full_run', label: 'Full Run' },
{ key: 'part_1', label: 'Run' },
{ key: 'part_2', label: 'Legacy 2' },
{ key: 'part_3', label: 'Legacy 3' },
{ key: 'full_run', label: 'Legacy Full' },
] as const).map((tab) => (
<button
key={tab.key}
@@ -730,12 +933,17 @@ function App() {
<div className="leaderboard-empty">
{gameMode === 'offline'
? 'Connect with an online character to compete in rankings.'
: 'Complete this difficulty to claim the first ranking.'}
: canShowCloudSync
? 'No leaderboard entries yet.'
: 'Complete this difficulty to claim the first ranking.'}
</div>
)}
</div>
</>
)}
</div>
)}
</aside>
</div>
</section>
)}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -156,7 +156,7 @@ export function AuthScreen({ onAuthenticated, serverMessage = '' }: Props) {
<h2>Play Offline</h2>
<p>
No account or connection required. Offline progress stays on
this device and is excluded from online leaderboards.
this device.
</p>
</div>
{offlineCharacterExists && (
File diff suppressed because it is too large Load Diff
+42 -3
View File
@@ -4,6 +4,7 @@ import {
type CharacterProfile,
type GameClass,
} from '../profile'
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
import { EquipmentScreen } from './EquipmentScreen'
import { TalentScreen } from './TalentScreen'
@@ -14,7 +15,8 @@ type Props = {
}
export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
const [activeTab, setActiveTab] = useState<'equipment' | 'talents' | 'class'>('class')
const [activeTab, setActiveTab] = useState<'equipment' | 'crafting' | 'talents' | 'class'>('class')
const { enabled: dualScreenEnabled } = useDualScreen()
const [classId, setClassId] = useState(profile.character.classId)
const [slots, setSlots] = useState<Array<number | null>>(profile.abilitySlots)
const [selectedSlot, setSelectedSlot] = useState(0)
@@ -38,7 +40,7 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
function chooseClass(nextClass: GameClass) {
const starterAbilities = nextClass.spells
.filter((ability) => ability.unlockLevel <= profile.character.level)
.slice(0, 5)
.slice(0, 6)
.map((ability) => ability.id)
setClassId(nextClass.id)
setSlots([...starterAbilities, ...Array(6 - starterAbilities.length).fill(null)])
@@ -63,6 +65,29 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
)
}
const classWorkshopState = useMemo<DualScreenWorkshopState | null>(() => {
if (activeTab !== 'class') return null
return {
mode: 'class',
title: 'Ability Library',
subtitle: gameClass.name,
summary: `Selected slot ${selectedSlot + 1}. ${message || 'Choose an ability for the active loadout.'}`,
items: gameClass.spells.map((ability) => {
const locked = ability.unlockLevel > profile.character.level
const equipped = slots.includes(ability.id)
return {
glyph: locked ? 'L' : ability.glyph,
title: ability.name,
meta: locked ? `Level ${ability.unlockLevel}` : `${ability.cost} ${gameClass.resourceName}`,
detail: ability.description,
status: equipped ? 'Equipped' : locked ? 'Locked' : '',
}
}),
}
}, [activeTab, gameClass, message, profile.character.level, selectedSlot, slots])
useDualScreenWorkshopPublisher(classWorkshopState, dualScreenEnabled)
async function persistChanges() {
saveScroll()
setSaving(true)
@@ -80,7 +105,7 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
return (
<section className="content-screen customize-screen">
<div className="screen-heading">
<div className="screen-heading customize-heading">
<div>
<p className="eyebrow">Character Workshop</p>
<h1>Customize Character</h1>
@@ -89,8 +114,10 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
</div>
<div className="customize-tabs" role="tablist" aria-label="Customize character sections">
<button className="back-button customize-tab-back" onClick={onBack} type="button">Back</button>
{([
{ key: 'equipment', label: 'Equipment' },
{ key: 'crafting', label: 'Crafting' },
{ key: 'talents', label: 'Talents' },
{ key: 'class', label: 'Class' },
] as const).map((tab) => (
@@ -110,6 +137,18 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
{activeTab === 'equipment' && (
<EquipmentScreen
embedded
mode="equipment"
showModeTabs={false}
profile={profile}
onUpdated={onSaved}
/>
)}
{activeTab === 'crafting' && (
<EquipmentScreen
embedded
mode="crafting"
showModeTabs={false}
profile={profile}
onUpdated={onSaved}
/>
+467 -117
View File
@@ -4,10 +4,12 @@ import {
craftItem,
equipItem,
loadProfile,
upgradeItem,
type CharacterProfile,
type EquipmentSlot,
type Item,
} from '../profile'
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
const SLOT_LABELS: Record<EquipmentSlot, string> = {
weapon: 'Weapon',
@@ -22,14 +24,50 @@ const SLOT_LABELS: Record<EquipmentSlot, string> = {
component: 'Component',
}
const EQUIPMENT_LIST_PAGE_SIZE = 3
const CRAFTING_LIST_PAGE_SIZE = 3
const CRAFTING_FILTER_SLOTS = (Object.keys(SLOT_LABELS) as EquipmentSlot[])
.filter((slot) => slot !== 'component')
const DIRECT_CRAFT_ITEM_LEVELS = new Set([1, 10, 20, 25])
type CraftingRecipe = CharacterProfile['craftingRecipes'][number]
function selectUpgradeRecipe(
paths: CharacterProfile['gearUpgradePaths'],
recipes: CraftingRecipe[],
item: Pick<Item, 'id' | 'slot' | 'itemLevel'>,
) {
const path = paths.find((candidate) => candidate.fromItemId === item.id)
if (path) {
const pathRecipe = recipes.find((recipe) => recipe.item.id === path.toItemId)
if (pathRecipe) return pathRecipe
}
const candidates = recipes.filter((recipe) =>
recipe.item.slot === item.slot
&& recipe.item.itemLevel > item.itemLevel
)
const nextItemLevel = Math.min(...candidates.map((recipe) => recipe.item.itemLevel))
if (!Number.isFinite(nextItemLevel)) return undefined
return candidates.find((recipe) => recipe.item.itemLevel === nextItemLevel)
}
type Props = {
profile: CharacterProfile
onBack?: () => void
onUpdated: (profile: CharacterProfile) => void
embedded?: boolean
mode?: 'equipment' | 'crafting'
showModeTabs?: boolean
}
export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }: Props) {
export function EquipmentScreen({
profile,
onBack,
onUpdated,
embedded = false,
mode,
showModeTabs = true,
}: Props) {
const { enabled: dualScreenEnabled } = useDualScreen()
const totalItemCount = profile.inventory.reduce(
(total, item) => total + item.quantity,
0,
@@ -43,17 +81,32 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
const [equipping, setEquipping] = useState(false)
const [breakingDown, setBreakingDown] = useState(false)
const [crafting, setCrafting] = useState(false)
const [upgrading, setUpgrading] = useState(false)
const [showSetBonuses, setShowSetBonuses] = useState(false)
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>('equipment')
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>(mode ?? 'equipment')
const [inventoryPage, setInventoryPage] = useState(0)
const [recipePage, setRecipePage] = useState(0)
const [message, setMessage] = useState('')
const scrollRef = useRef<number>(0)
const selectedItem = profile.inventory.find((item) => item.id === selectedItemId)
const firstRecipe = profile.craftingRecipes.find((recipe) => recipe.canCraft)
?? profile.craftingRecipes[0]
const craftableRecipes = profile.craftingRecipes.filter((recipe) =>
DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel),
)
const firstRecipe = craftableRecipes.find((recipe) => recipe.canCraft)
?? craftableRecipes[0]
const [selectedRecipeId, setSelectedRecipeId] = useState<number | null>(
firstRecipe?.id ?? null,
)
const selectedRecipe = profile.craftingRecipes.find((recipe) => recipe.id === selectedRecipeId)
const selectedRecipeRequiresUpgrade = selectedRecipe
? !DIRECT_CRAFT_ITEM_LEVELS.has(selectedRecipe.item.itemLevel)
: false
const selectedItemRecipe = selectedItem
? profile.craftingRecipes.find((recipe) => recipe.item.id === selectedItem.id)
: undefined
const upgradeRecipe = selectedItem && selectedItemRecipe
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, selectedItem)
: undefined
const equippedBySlot = useMemo(
() => new Map(
profile.inventory
@@ -75,16 +128,26 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
(total, item) => total + item.quantity,
0,
)
const inventoryPageCount = Math.max(
1,
Math.ceil(visibleInventory.length / EQUIPMENT_LIST_PAGE_SIZE),
)
const inventoryPageItems = visibleInventory.slice(
inventoryPage * EQUIPMENT_LIST_PAGE_SIZE,
(inventoryPage + 1) * EQUIPMENT_LIST_PAGE_SIZE,
)
const [slotFilter, setSlotFilter] = useState<EquipmentSlot | 'all'>('all')
const [levelFilter, setLevelFilter] = useState<number | null>(null)
const availableLevels = useMemo(
() => [...new Set(profile.craftingRecipes.map((r) => r.item.itemLevel))].sort((a, b) => b - a),
() => [...new Set(profile.craftingRecipes
.filter((r) => DIRECT_CRAFT_ITEM_LEVELS.has(r.item.itemLevel))
.map((r) => r.item.itemLevel))].sort((a, b) => b - a),
[profile.craftingRecipes],
)
const filteredRecipes = useMemo(
() => {
let result = [...profile.craftingRecipes]
let result = profile.craftingRecipes.filter((r) => DIRECT_CRAFT_ITEM_LEVELS.has(r.item.itemLevel))
if (slotFilter !== 'all') result = result.filter((r) => r.item.slot === slotFilter)
if (levelFilter !== null) result = result.filter((r) => r.item.itemLevel === levelFilter)
result.sort((a, b) => b.item.itemLevel - a.item.itemLevel)
@@ -92,17 +155,60 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
},
[profile.craftingRecipes, slotFilter, levelFilter],
)
const readyRecipeCount = filteredRecipes.filter((recipe) => recipe.canCraft).length
const slotRecipeCounts = useMemo(
() => new Map(
(Object.keys(SLOT_LABELS) as EquipmentSlot[]).map((slot) => [
slot,
profile.craftingRecipes.filter((recipe) =>
recipe.item.slot === slot
&& DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel),
).length,
]),
),
[profile.craftingRecipes],
)
const recipePageCount = Math.max(
1,
Math.ceil(filteredRecipes.length / CRAFTING_LIST_PAGE_SIZE),
)
const recipePageItems = filteredRecipes.slice(
recipePage * CRAFTING_LIST_PAGE_SIZE,
(recipePage + 1) * CRAFTING_LIST_PAGE_SIZE,
)
useEffect(() => {
window.scrollTo(0, scrollRef.current)
}, [profile])
useEffect(() => {
setInventoryPage((current) => Math.min(current, inventoryPageCount - 1))
}, [inventoryPageCount])
useEffect(() => {
setRecipePage((current) => Math.min(current, recipePageCount - 1))
}, [recipePageCount])
useEffect(() => {
if (filteredRecipes.length === 0) {
setSelectedRecipeId(null)
return
}
if (!filteredRecipes.some((recipe) => recipe.id === selectedRecipeId)) {
setSelectedRecipeId(filteredRecipes[0].id)
}
}, [filteredRecipes, selectedRecipeId])
useEffect(() => {
if (equipmentTab === 'crafting') {
loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {})
}
}, [equipmentTab])
useEffect(() => {
if (mode) setEquipmentTab(mode)
}, [mode])
function saveScroll() {
scrollRef.current = window.scrollY
}
@@ -160,6 +266,160 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
}
}
async function upgradeSelected() {
if (!selectedItem || !upgradeRecipe) return
saveScroll()
setUpgrading(true)
setMessage('')
try {
const updated = await upgradeItem(selectedItem.id)
onUpdated(updated)
setSelectedItemId(upgradeRecipe.item.id)
setMessage(`${selectedItem.name} upgraded to ${upgradeRecipe.item.name}.`)
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : 'Unable to upgrade item.')
} finally {
setUpgrading(false)
}
}
function renderEquipmentActions() {
if (!selectedItem) {
return <p>Select an item to inspect it.</p>
}
if (selectedItem.slot === 'component') {
return <p className="component-note">Used in crafting.</p>
}
return (
<>
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
<button
className="primary-button"
disabled={selectedItem.equipped || equipping || breakingDown || upgrading}
onClick={equipSelected}
type="button"
>
{selectedItem.equipped ? 'Equipped' : equipping ? 'Equipping...' : 'Equip Item'}
</button>
{upgradeRecipe && (
<button
className="primary-button"
disabled={!upgradeRecipe.canCraft || equipping || breakingDown || upgrading}
onClick={upgradeSelected}
type="button"
>
{upgrading ? 'Upgrading...' : `Upgrade to iLvl ${upgradeRecipe.item.itemLevel}`}
</button>
)}
{(!selectedItem.equipped || selectedItem.quantity > 1) && (
<button
className="breakdown-button"
disabled={equipping || breakingDown || upgrading}
onClick={breakdownSelected}
type="button"
>
{breakingDown
? 'Breaking Down...'
: selectedItem.quantity > 1
? 'Break Down Duplicate'
: 'Break Down'}
</button>
)}
</>
)
}
const workshopState = useMemo<DualScreenWorkshopState>(() => {
if (equipmentTab === 'crafting') {
if (!selectedRecipe) {
return {
mode: 'crafting',
title: 'Craft Output',
subtitle: 'No recipe selected',
items: [],
}
}
return {
mode: 'crafting',
title: selectedRecipe.item.name,
subtitle: `${SLOT_LABELS[selectedRecipe.item.slot]} - Item Level ${selectedRecipe.item.itemLevel}`,
summary: selectedRecipe.item.description,
items: [
{
glyph: selectedRecipe.item.glyph,
title: 'Craft Output',
meta: `+${selectedRecipe.item.healingPower} Healing Power / +${selectedRecipe.item.maxResourceBonus} Max Resource`,
status: selectedRecipe.canCraft ? 'Ready' : 'Missing components',
},
...selectedRecipe.components.map((component) => ({
glyph: component.item.glyph,
title: component.item.name,
meta: `Item Level ${component.item.itemLevel}`,
status: `${component.owned}/${component.quantity}`,
})),
],
}
}
if (!selectedItem) {
return {
mode: 'equipment',
title: 'Equipment Detail',
subtitle: 'No item selected',
items: [],
}
}
return {
mode: 'equipment',
title: selectedItem.slot === 'component' ? 'Crafting Component' : selectedItem.name,
subtitle: `${SLOT_LABELS[selectedItem.slot]} - Item Level ${selectedItem.itemLevel}`,
summary: selectedItem.description,
items: selectedItem.slot === 'component'
? [{
glyph: selectedItem.glyph,
title: selectedItem.name,
meta: `Owned: ${selectedItem.quantity}`,
status: 'Component',
}]
: [
{
glyph: selectedItem.glyph,
title: selectedItem.name,
meta: `+${selectedItem.healingPower} Healing Power / +${selectedItem.maxResourceBonus} Max Resource`,
status: selectedItem.equipped ? 'Equipped' : 'Inventory',
},
...(comparisonItem && comparisonItem.id !== selectedItem.id
? [{
glyph: comparisonItem.glyph,
title: comparisonItem.name,
meta: `+${comparisonItem.healingPower} Healing Power / +${comparisonItem.maxResourceBonus} Max Resource`,
status: 'Currently Equipped',
}]
: [{
title: selectedItem.equipped ? 'Already Equipped' : 'Empty Slot',
status: 'Comparison',
}]),
...(upgradeRecipe
? [
{
glyph: upgradeRecipe.item.glyph,
title: `Upgrade to ${upgradeRecipe.item.name}`,
meta: `Item Level ${upgradeRecipe.item.itemLevel}`,
status: upgradeRecipe.canCraft ? 'Ready' : 'Missing materials',
},
...upgradeRecipe.components.map((component) => ({
glyph: component.item.glyph,
title: component.item.name,
meta: `Required for upgrade`,
status: `${component.owned}/${component.quantity}`,
})),
]
: []),
],
}
}, [comparisonItem, equipmentTab, selectedItem, selectedRecipe, upgradeRecipe])
useDualScreenWorkshopPublisher(workshopState, dualScreenEnabled)
const content = (
<>
{!embedded && (
@@ -186,22 +446,24 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<GearStat value={`+${profile.gearStats.maxResourceBonus}`} label={`Max ${profile.character.resourceName}`} />
</div>
<nav className="equipment-tabs">
<button
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
onClick={() => setEquipmentTab('equipment')}
type="button"
>
Equipment
</button>
<button
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
onClick={() => setEquipmentTab('crafting')}
type="button"
>
Crafting
</button>
</nav>
{showModeTabs && (
<nav className="equipment-tabs">
<button
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
onClick={() => setEquipmentTab('equipment')}
type="button"
>
Equipment
</button>
<button
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
onClick={() => setEquipmentTab('crafting')}
type="button"
>
Crafting
</button>
</nav>
)}
{equipmentTab === 'equipment' ? (
<>
@@ -210,9 +472,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
selectedItem.slot === 'component' ? (
<>
<ItemDetail title="Crafting Component" item={selectedItem} />
<div className="equip-action">
<p className="component-note">Used in crafting.</p>
</div>
</>
) : (
<>
@@ -226,31 +485,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<h2>{selectedItem.equipped ? 'Already Equipped' : 'Empty Slot'}</h2>
</div>
)}
<div className="equip-action">
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
<button
className="primary-button"
disabled={selectedItem.equipped || equipping || breakingDown}
onClick={equipSelected}
type="button"
>
{selectedItem.equipped ? 'Equipped' : equipping ? 'Equipping...' : 'Equip Item'}
</button>
{(!selectedItem.equipped || selectedItem.quantity > 1) && (
<button
className="breakdown-button"
disabled={equipping || breakingDown}
onClick={breakdownSelected}
type="button"
>
{breakingDown
? 'Breaking Down...'
: selectedItem.quantity > 1
? 'Break Down Duplicate'
: 'Break Down'}
</button>
)}
</div>
</>
)
) : (
@@ -258,6 +492,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
)}
</section>
<section className="equipment-action-strip">
{renderEquipmentActions()}
</section>
<div className="equipment-layout">
<section className="equipped-panel">
<EquipmentHeading eyebrow="Currently Worn" title="Equipment Slots" />
@@ -270,6 +508,7 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
key={slot}
onClick={() => {
setSelectedSlot(slot)
setInventoryPage(0)
const firstSlotItem = profile.inventory.find(
(candidate) => candidate.slot === slot,
)
@@ -302,14 +541,17 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
{selectedSlot && (
<button
className="inventory-filter-clear"
onClick={() => setSelectedSlot(null)}
onClick={() => {
setSelectedSlot(null)
setInventoryPage(0)
}}
type="button"
>
Show All Items
</button>
)}
<div className="inventory-list">
{visibleInventory.map((item) => (
{inventoryPageItems.map((item) => (
<button
className={`${selectedItemId === item.id ? 'selected' : ''} rarity-${item.rarity}`}
key={item.id}
@@ -333,6 +575,15 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
</p>
)}
</div>
{visibleInventory.length > EQUIPMENT_LIST_PAGE_SIZE && (
<ListPager
label={`Page ${inventoryPage + 1} / ${inventoryPageCount}`}
onNext={() => setInventoryPage((current) => Math.min(inventoryPageCount - 1, current + 1))}
onPrevious={() => setInventoryPage((current) => Math.max(0, current - 1))}
nextDisabled={inventoryPage >= inventoryPageCount - 1}
previousDisabled={inventoryPage <= 0}
/>
)}
</section>
</div>
</>
@@ -340,87 +591,164 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<section className="crafting-panel">
<EquipmentHeading
eyebrow="Crafting"
title="Recipes"
detail={`${filteredRecipes.filter((recipe) => recipe.canCraft).length} ready`}
title="Workbench"
detail={`${readyRecipeCount} ready / ${filteredRecipes.length} shown`}
/>
<div className="crafting-filter-bar">
<select
className="filter-select"
value={slotFilter}
onChange={(e) => setSlotFilter(e.target.value as EquipmentSlot | 'all')}
>
<option value="all">All Slots</option>
{(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => (
<option key={slot} value={slot}>{label}</option>
))}
</select>
<select
className="filter-select"
value={levelFilter ?? ''}
onChange={(e) => setLevelFilter(e.target.value === '' ? null : Number(e.target.value))}
>
<option value="">All Levels</option>
{availableLevels.map((level) => (
<option key={level} value={level}>Item Level {level}</option>
))}
</select>
</div>
{filteredRecipes.length === 0 && (
<p className="inventory-empty">No crafting recipes match filters.</p>
)}
{filteredRecipes.length > 0 && (
<div className="crafting-layout">
<div className="crafting-list">
{filteredRecipes.map((recipe) => (
<div className="crafting-layout">
<aside className="crafting-filters">
<EquipmentHeading
eyebrow="Slots"
title="Gear Slots"
detail={slotFilter === 'all' ? 'All' : SLOT_LABELS[slotFilter]}
/>
<div>
<div className="crafting-filter-grid">
<button
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
key={recipe.id}
onClick={() => setSelectedRecipeId(recipe.id)}
className={slotFilter === 'all' ? 'active' : ''}
onClick={() => {
setSlotFilter('all')
setRecipePage(0)
}}
type="button"
>
<span>{recipe.item.glyph}</span>
<div>
<strong>{recipe.item.name}</strong>
<small>
{SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel}
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
</small>
</div>
<i>{recipe.canCraft ? 'Ready' : 'Needs materials'}</i>
<strong>All</strong>
<span>{profile.craftingRecipes.filter((recipe) => DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel)).length}</span>
</button>
))}
{CRAFTING_FILTER_SLOTS.map((slot) => (
<button
className={slotFilter === slot ? 'active' : ''}
disabled={(slotRecipeCounts.get(slot) ?? 0) === 0}
key={slot}
onClick={() => {
setSlotFilter(slot)
setRecipePage(0)
}}
type="button"
>
<strong>{SLOT_LABELS[slot]}</strong>
<span>{slotRecipeCounts.get(slot) ?? 0}</span>
</button>
))}
</div>
</div>
{selectedRecipe && (
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
<div className="crafting-components">
{selectedRecipe.components.map((component) => (
<div
className={component.owned >= component.quantity ? 'ready' : 'missing'}
key={component.item.id}
>
<span>{component.item.glyph}</span>
<strong>{component.item.name}</strong>
<i>{component.owned}/{component.quantity}</i>
<div>
<p className="eyebrow">Item Level</p>
<div className="crafting-level-row">
<button
className={levelFilter === null ? 'active' : ''}
onClick={() => {
setLevelFilter(null)
setRecipePage(0)
}}
type="button"
>
All
</button>
{availableLevels.map((level) => (
<button
className={levelFilter === level ? 'active' : ''}
key={level}
onClick={() => {
setLevelFilter(level)
setRecipePage(0)
}}
type="button"
>
{level}
</button>
))}
</div>
</div>
</aside>
<section className="crafting-available-panel">
<section className="crafting-list-panel">
<EquipmentHeading
eyebrow="Available Gear"
title={slotFilter === 'all' ? 'Craftable Gear' : SLOT_LABELS[slotFilter]}
detail={`Page ${recipePage + 1}/${recipePageCount}`}
/>
{filteredRecipes.length === 0 ? (
<p className="inventory-empty">No recipes match filters.</p>
) : (
<div className="crafting-list">
{recipePageItems.map((recipe) => (
<button
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
key={recipe.id}
onClick={() => setSelectedRecipeId(recipe.id)}
type="button"
>
<span>{recipe.item.glyph}</span>
<div>
<strong>{recipe.item.name}</strong>
<small>
{SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel}
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
</small>
</div>
))}
<i className={recipe.canCraft ? 'ready' : 'missing'}>
{recipe.canCraft ? 'Ready' : 'Needs materials'}
</i>
</button>
))}
</div>
)}
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
<ListPager
label={`Page ${recipePage + 1} / ${recipePageCount}`}
onNext={() => setRecipePage((current) => Math.min(recipePageCount - 1, current + 1))}
onPrevious={() => setRecipePage((current) => Math.max(0, current - 1))}
nextDisabled={recipePage >= recipePageCount - 1}
previousDisabled={recipePage <= 0}
/>
)}
<div className="crafting-action-row">
<button
className="primary-button"
disabled={!selectedRecipe.canCraft || crafting}
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
onClick={craftSelected}
type="button"
>
{crafting ? 'Crafting...' : 'Craft Item'}
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button>
</div>
)}
</div>
)}
</section>
<section className="crafting-detail-panel">
{selectedRecipe ? (
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
<div className="crafting-detail-heading">
<p className="eyebrow">Materials</p>
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
</div>
<div className="crafting-components">
{selectedRecipe.components.length === 0 && (
<p className="inventory-empty">No materials configured. Crafting disabled.</p>
)}
{selectedRecipe.components.map((component) => (
<div
className={component.owned >= component.quantity ? 'ready' : 'missing'}
key={component.item.id}
>
<span>{component.item.glyph}</span>
<strong>{component.item.name}</strong>
<i>{component.owned}/{component.quantity}</i>
</div>
))}
</div>
</div>
) : (
<p className="inventory-empty">Select a recipe.</p>
)}
</section>
</section>
</div>
</section>
)}
{profile.setBonuses.length > 0 && (
{equipmentTab === 'equipment' && profile.setBonuses.length > 0 && (
<section className="set-bonus-panel">
<div className="equipment-heading toggle-heading">
<div>
@@ -456,16 +784,38 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
)
if (embedded) {
return <div className="equipment-screen embedded-screen">{content}</div>
return <div className={`equipment-screen embedded-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>{content}</div>
}
return (
<section className="content-screen equipment-screen">
<section className={`content-screen equipment-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>
{content}
</section>
)
}
function ListPager({
label,
nextDisabled,
previousDisabled,
onNext,
onPrevious,
}: {
label: string
nextDisabled: boolean
previousDisabled: boolean
onNext: () => void
onPrevious: () => void
}) {
return (
<div className="list-pager">
<button disabled={previousDisabled} onClick={onPrevious} type="button">Prev</button>
<span>{label}</span>
<button disabled={nextDisabled} onClick={onNext} type="button">Next</button>
</div>
)
}
function GearStat({ value, label }: { value: string; label: string }) {
return (
<div className="gear-stat">
File diff suppressed because it is too large Load Diff
+155 -126
View File
@@ -24,6 +24,7 @@ import {
export function SettingsScreen({ onBack }: { onBack: () => void }) {
const [device, setDevice] = useState<InputDevice>('controller')
const [settingsTab, setSettingsTab] = useState<'display' | 'input' | 'bindings'>('display')
const [displayMessage, setDisplayMessage] = useState('')
const [androidDisplays, setAndroidDisplays] = useState<AndroidDisplay[]>([])
const {
@@ -50,6 +51,7 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
'targetParty3',
'targetParty4',
'targetParty5',
'targetParty6',
'toggleTargetGroup',
])
const visibleActions = INPUT_ACTIONS.filter((action) => (
@@ -95,138 +97,165 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
<button className="back-button" onClick={onBack} type="button">Back</button>
</div>
<section className="dual-screen-settings">
<div>
<p className="eyebrow">Display</p>
<h2>AYN Thor Dual-Screen Mode</h2>
<p>
The upper display shows enemy and party health. The lower display
keeps targeting, resources, skills, and cooldowns.
</p>
</div>
<div className="dual-screen-actions">
<nav className="settings-tabs" role="tablist" aria-label="Settings sections">
{([
{ key: 'display', label: 'Display' },
{ key: 'input', label: 'Input' },
{ key: 'bindings', label: 'Bindings' },
] as const).map((tab) => (
<button
className={dualScreenEnabled ? 'selected' : ''}
onClick={() => {
setDualScreenEnabled(!dualScreenEnabled)
setDisplayMessage('')
}}
aria-selected={settingsTab === tab.key}
className={settingsTab === tab.key ? 'selected' : ''}
key={tab.key}
onClick={() => setSettingsTab(tab.key)}
role="tab"
type="button"
>
{dualScreenEnabled ? 'Dual-Screen Enabled' : 'Enable Dual-Screen'}
</button>
<button onClick={launchTopDisplay} type="button">
{topDisplayConnected ? 'Companion Connected' : 'Open Companion Display'}
</button>
</div>
<small>
{displayMessage || (
topDisplayConnected
? 'The companion display is connected and receiving live combat data.'
: 'Open the companion display before starting combat.'
)}
</small>
{nativeDualScreen && androidDisplays.length > 0 && (
<div className="android-display-list">
{androidDisplays.map((display) => (
<span key={display.id}>
<strong>{display.isCurrent ? 'Current' : 'Secondary'} #{display.id}</strong>
{display.width}×{display.height} at {Math.round(display.refreshRate)} Hz
{display.isPresentation ? ' - Presentation' : ''}
</span>
))}
</div>
)}
</section>
<div className="settings-heading">
<div>
<p className="eyebrow">Input</p>
<h2>Keybindings</h2>
</div>
<p>Select an action, then press the new key or controller control.</p>
</div>
<section className="controller-preferences">
<div>
<p className="eyebrow">Targeting</p>
<h3>Direct Party Keybinds</h3>
<p>
Assign party slots directly. In raids, use the group-switch binding
to alternate between members 1-5 and 6-10.
</p>
</div>
<button
aria-pressed={directPartyTargeting}
className={directPartyTargeting ? 'selected' : ''}
onClick={() => setDirectPartyTargeting(!directPartyTargeting)}
type="button"
>
{directPartyTargeting ? 'Direct Targeting On' : 'Direct Targeting Off'}
</button>
<div className="controller-icon-options">
<span>Controller Icons</span>
{(['xbox', 'playstation', 'nintendo'] as const).map((style) => (
<button
aria-pressed={controllerIconStyle === style}
className={controllerIconStyle === style ? 'selected' : ''}
key={style}
onClick={() => setControllerIconStyle(style)}
type="button"
>
<ControllerStylePreview iconStyle={style} />
<span className="controller-style-name">{CONTROLLER_STYLE_LABELS[style]}</span>
</button>
))}
</div>
</section>
<div className="binding-tabs">
<button
className={device === 'controller' ? 'selected' : ''}
onClick={() => setDevice('controller')}
type="button"
>
Controller
</button>
<button
className={device === 'pc' ? 'selected' : ''}
onClick={() => setDevice('pc')}
type="button"
>
PC
</button>
</div>
<div className="binding-list">
{visibleActions.map((action) => (
<button
className={capture?.device === device && capture.action === action ? 'listening' : ''}
key={action}
onClick={() => beginCapture(device, action)}
type="button"
>
<span>{ACTION_LABELS[action]}</span>
<kbd>
{capture?.device === device && capture.action === action
? 'Press a control...'
: (
<ControllerBindingLabel
binding={bindings[device][action]}
iconStyle={controllerIconStyle}
/>
)}
</kbd>
{tab.label}
</button>
))}
</div>
</nav>
<footer className="settings-footer">
<span>Bindings are saved automatically on this device.</span>
<button className="text-button" onClick={() => resetBindings(device)} type="button">
Reset {device === 'pc' ? 'PC' : 'Controller'} Defaults
</button>
</footer>
{settingsTab === 'display' && (
<section className="dual-screen-settings settings-tab-panel">
<div>
<p className="eyebrow">Display</p>
<h2>AYN Thor Dual-Screen Mode</h2>
<p>
The upper display shows enemy and party health. The lower display
keeps targeting, resources, skills, and cooldowns.
</p>
</div>
<div className="dual-screen-actions">
<button
className={dualScreenEnabled ? 'selected' : ''}
onClick={() => {
setDualScreenEnabled(!dualScreenEnabled)
setDisplayMessage('')
}}
type="button"
>
{dualScreenEnabled ? 'Dual-Screen Enabled' : 'Enable Dual-Screen'}
</button>
<button onClick={launchTopDisplay} type="button">
{topDisplayConnected ? 'Companion Connected' : 'Open Companion Display'}
</button>
</div>
<small>
{displayMessage || (
topDisplayConnected
? 'The companion display is connected and receiving live combat data.'
: 'Open the companion display before starting combat.'
)}
</small>
{nativeDualScreen && androidDisplays.length > 0 && (
<div className="android-display-list">
{androidDisplays.map((display) => (
<span key={display.id}>
<strong>{display.isCurrent ? 'Current' : 'Secondary'} #{display.id}</strong>
{display.width}x{display.height} at {Math.round(display.refreshRate)} Hz
{display.isPresentation ? ' - Presentation' : ''}
</span>
))}
</div>
)}
</section>
)}
{settingsTab === 'input' && (
<section className="controller-preferences settings-tab-panel">
<div>
<p className="eyebrow">Targeting</p>
<h3>Direct Party Keybinds</h3>
<p>
Assign party slots directly. In raids, use the group-switch binding
to alternate between members 1-6, 7-12, and 13-18.
</p>
</div>
<button
aria-pressed={directPartyTargeting}
className={directPartyTargeting ? 'selected' : ''}
onClick={() => setDirectPartyTargeting(!directPartyTargeting)}
type="button"
>
{directPartyTargeting ? 'Direct Targeting On' : 'Direct Targeting Off'}
</button>
<div className="controller-icon-options">
<span>Controller Icons</span>
{(['xbox', 'playstation', 'nintendo'] as const).map((style) => (
<button
aria-pressed={controllerIconStyle === style}
className={controllerIconStyle === style ? 'selected' : ''}
key={style}
onClick={() => setControllerIconStyle(style)}
type="button"
>
<ControllerStylePreview iconStyle={style} />
<span className="controller-style-name">{CONTROLLER_STYLE_LABELS[style]}</span>
</button>
))}
</div>
</section>
)}
{settingsTab === 'bindings' && (
<section className="settings-bindings-panel settings-tab-panel">
<div className="settings-heading">
<div>
<p className="eyebrow">Input</p>
<h2>Keybindings</h2>
</div>
<p>Select an action, then press the new key or controller control.</p>
</div>
<div className="binding-tabs">
<button
className={device === 'controller' ? 'selected' : ''}
onClick={() => setDevice('controller')}
type="button"
>
Controller
</button>
<button
className={device === 'pc' ? 'selected' : ''}
onClick={() => setDevice('pc')}
type="button"
>
PC
</button>
</div>
<div className="binding-list">
{visibleActions.map((action) => (
<button
className={capture?.device === device && capture.action === action ? 'listening' : ''}
key={action}
onClick={() => beginCapture(device, action)}
type="button"
>
<span>{ACTION_LABELS[action]}</span>
<kbd>
{capture?.device === device && capture.action === action
? 'Press a control...'
: (
<ControllerBindingLabel
binding={bindings[device][action]}
iconStyle={controllerIconStyle}
/>
)}
</kbd>
</button>
))}
</div>
<footer className="settings-footer">
<span>Bindings are saved automatically on this device.</span>
<button className="text-button" onClick={() => resetBindings(device)} type="button">
Reset {device === 'pc' ? 'PC' : 'Controller'} Defaults
</button>
</footer>
</section>
)}
{capture && (
<div className="binding-capture" role="dialog" aria-modal="true">
+206 -97
View File
@@ -1,10 +1,11 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
allocateTalent,
resetTalents,
type CharacterProfile,
type Talent,
} from '../profile'
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
type Props = {
profile: CharacterProfile
@@ -13,178 +14,286 @@ type Props = {
embedded?: boolean
}
const EFFECT_SLOT_LEVELS = [5, 10, 15, 20] as const
const EFFECT_CLASS_ID = 1
const EFFECTS_PER_PAGE = 8
const EFFECT_SOURCE_LABELS: Record<string, string> = {
mend: 'Mend',
radiance: 'Radiance',
shield: 'Shield',
}
function effectSource(effectType: string) {
if (effectType.startsWith('mend_')) return 'mend'
if (effectType.startsWith('radiance_')) return 'radiance'
if (effectType.startsWith('shield_') || effectType.startsWith('shielded_')) return 'shield'
return effectType
}
function effectCapacity(level: number) {
return EFFECT_SLOT_LEVELS.filter((slotLevel) => level >= slotLevel).length
}
function activeEffects(talents: Talent[]) {
return talents.filter((talent) => talent.rank > 0)
}
export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: Props) {
const { enabled: dualScreenEnabled } = useDualScreen()
const [busyTalentId, setBusyTalentId] = useState<number | null>(null)
const [resetting, setResetting] = useState(false)
const [selectedTalentId, setSelectedTalentId] = useState<number | null>(null)
const [effectPage, setEffectPage] = useState(0)
const [message, setMessage] = useState('')
const scrollRef = useRef<number>(0)
const gameClass = profile.classes.find(
(candidate) => candidate.id === profile.character.classId,
)!
const classPointsSpent = gameClass.talents.reduce(
(total, talent) => total + talent.rank,
0,
const isEffectClass = gameClass.id === EFFECT_CLASS_ID
const capacity = isEffectClass ? effectCapacity(profile.character.level) : 0
const selectedEffects = activeEffects(gameClass.talents)
const selectedTalent = gameClass.talents.find((talent) => talent.id === selectedTalentId)
?? selectedEffects[0]
?? gameClass.talents[0]
?? null
const effectPageCount = Math.max(1, Math.ceil(gameClass.talents.length / EFFECTS_PER_PAGE))
const visibleTalents = gameClass.talents.slice(
effectPage * EFFECTS_PER_PAGE,
effectPage * EFFECTS_PER_PAGE + EFFECTS_PER_PAGE,
)
const tiers = Array.from(
new Set(gameClass.talents.map((talent) => talent.tier)),
).sort((a, b) => a - b)
useEffect(() => {
window.scrollTo(0, scrollRef.current)
}, [profile])
useEffect(() => {
if (selectedTalentId && gameClass.talents.some((talent) => talent.id === selectedTalentId)) return
setSelectedTalentId(selectedTalent?.id ?? null)
}, [gameClass.talents, selectedTalent?.id, selectedTalentId])
useEffect(() => {
setEffectPage((page) => Math.min(page, effectPageCount - 1))
}, [effectPageCount])
function saveScroll() {
scrollRef.current = window.scrollY
}
function lowerTierPoints(talent: Talent) {
return gameClass.talents
.filter((candidate) => candidate.tier < talent.tier)
.reduce((total, candidate) => total + candidate.rank, 0)
}
function lockReason(talent: Talent) {
if (talent.rank >= talent.maxRank) return 'Maximum rank'
const requiredTierPoints = (talent.tier - 1) * 5
if (lowerTierPoints(talent) < requiredTierPoints) {
return `Requires ${requiredTierPoints} earlier-tier points`
if (!isEffectClass) return 'Coming soon'
if (talent.rank > 0) return ''
const source = effectSource(talent.effectType)
const sourceConflict = selectedEffects.find((effect) => effectSource(effect.effectType) === source)
if (sourceConflict) return `${EFFECT_SOURCE_LABELS[source] ?? source} already selected`
if (capacity <= 0) return 'Unlocks at level 5'
if (selectedEffects.length >= capacity) {
return `Active slots full (${capacity}/${capacity})`
}
if (talent.prerequisiteTalentId) {
const prerequisite = gameClass.talents.find(
(candidate) => candidate.id === talent.prerequisiteTalentId,
)
if ((prerequisite?.rank ?? 0) < talent.prerequisiteRank) {
return `Requires ${talent.prerequisiteName} rank ${talent.prerequisiteRank}`
}
}
if (profile.character.talentPoints <= 0) return 'No points available'
return ''
}
async function purchaseRank(talent: Talent) {
async function toggleEffect(talent: Talent) {
saveScroll()
setBusyTalentId(talent.id)
setMessage('')
try {
const updated = await allocateTalent(talent.id)
onUpdated(updated)
setMessage(`${talent.name} increased to rank ${talent.rank + 1}.`)
setSelectedTalentId(talent.id)
setMessage(talent.rank > 0 ? `${talent.name} removed.` : `${talent.name} activated.`)
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : 'Unable to allocate talent.')
setMessage(reason instanceof Error ? reason.message : 'Unable to update spell effect.')
} finally {
setBusyTalentId(null)
}
}
async function refundTree() {
async function clearEffects() {
saveScroll()
setResetting(true)
setMessage('')
try {
const updated = await resetTalents()
onUpdated(updated)
setMessage('All points in this talent tree were refunded.')
setMessage('Spell effects cleared.')
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : 'Unable to reset talents.')
setMessage(reason instanceof Error ? reason.message : 'Unable to clear spell effects.')
} finally {
setResetting(false)
}
}
const workshopState = useMemo<DualScreenWorkshopState | null>(() => {
if (!isEffectClass) return null
return {
mode: 'talents',
title: 'Spell Effects',
subtitle: `${selectedEffects.length}/${capacity} active`,
summary: selectedTalent
? `${selectedTalent.name}: ${selectedTalent.description}`
: 'Choose effects to modify your spells.',
items: gameClass.talents.map((talent) => ({
glyph: talent.glyph,
title: talent.name,
meta: talent.rank > 0 ? 'Active' : lockReason(talent) || 'Available',
detail: talent.description,
status: talent.rank > 0 ? 'Selected' : '',
})),
}
}, [capacity, gameClass.talents, isEffectClass, selectedEffects.length, selectedTalent])
useDualScreenWorkshopPublisher(workshopState, dualScreenEnabled)
const content = (
<>
{!embedded && (
<div className="screen-heading">
<div>
<p className="eyebrow">Character Growth</p>
<h1>Talents</h1>
<h1>Spell Effects</h1>
</div>
<button className="back-button" onClick={onBack} type="button">Back</button>
</div>
)}
<div className="talent-toolbar">
<div className="talent-toolbar spell-effect-toolbar">
<div className="talent-class-summary">
<span style={{ borderColor: gameClass.themeColor, color: gameClass.themeColor }}>
{gameClass.name[0]}
</span>
<div>
<p className="eyebrow">{gameClass.name} Tree</p>
<h2>Shape Your Healing Style</h2>
<p className="eyebrow">{gameClass.name} Effects</p>
<h2>Modify Your Spells</h2>
</div>
</div>
<div className="talent-points">
<strong>{profile.character.talentPoints}</strong>
<span>Available</span>
<small>{classPointsSpent} spent in this tree</small>
<strong>{selectedEffects.length}/{capacity}</strong>
<span>Active</span>
<small>Slots unlock at levels 5, 10, 15, 20</small>
</div>
</div>
<div className="talent-tree">
{tiers.map((tier) => {
const requiredPoints = (tier - 1) * 5
return (
<section className="talent-tier" key={tier}>
<div className="tier-label">
<span>Tier {tier}</span>
<small>
{tier === 1 ? 'Open' : `${requiredPoints} earlier-tier points`}
</small>
{!isEffectClass ? (
<div className="talent-empty-state">
<h2>Spell effects coming soon for {gameClass.name}.</h2>
<p>This replacement system starts with the first class.</p>
</div>
) : (
<div className="spell-effect-layout">
<section className="effect-slots-panel">
<p className="eyebrow">Active Slots</p>
{EFFECT_SLOT_LEVELS.map((level, index) => {
const effect = selectedEffects[index]
const unlocked = profile.character.level >= level
return (
<button
className={`effect-slot ${effect ? 'filled' : ''} ${unlocked ? '' : 'locked'}`}
disabled={!effect}
key={level}
onClick={() => effect && setSelectedTalentId(effect.id)}
type="button"
>
<span>Lv {level}</span>
<strong>{effect?.name ?? (unlocked ? 'Empty Slot' : 'Locked')}</strong>
<small>{effect?.description ?? (unlocked ? 'Choose an effect from the pool.' : `Reach level ${level}.`)}</small>
</button>
)
})}
</section>
<section className="effect-pool-panel">
<div className="effect-panel-heading">
<div>
<p className="eyebrow">Effect Pool</p>
<h2>Choose and Swap</h2>
</div>
<div className="tier-talents">
{gameClass.talents
.filter((talent) => talent.tier === tier)
.sort((a, b) => a.branch - b.branch)
.map((talent) => {
const reason = lockReason(talent)
const isBusy = busyTalentId === talent.id
return (
<article
className={`talent-node ${reason ? 'locked' : 'available'} ${talent.rank > 0 ? 'invested' : ''}`}
key={talent.id}
style={{ gridColumn: talent.branch }}
>
<div className="talent-node-header">
<span>{talent.glyph}</span>
<div>
<strong>{talent.name}</strong>
<small>Rank {talent.rank}/{talent.maxRank}</small>
</div>
</div>
<p>{talent.description}</p>
<div className="rank-pips">
{Array.from({ length: talent.maxRank }, (_, index) => (
<i className={index < talent.rank ? 'filled' : ''} key={index} />
))}
</div>
<button
disabled={Boolean(reason) || isBusy}
onClick={() => purchaseRank(talent)}
type="button"
>
{isBusy ? 'Saving...' : reason || 'Add Rank'}
</button>
</article>
)
})}
<span>{selectedEffects.length}/{capacity} active</span>
</div>
<div className="selected-effect-strip">
<div>
<p className="eyebrow">Selected Effect</p>
{selectedTalent ? (
<>
<strong>{selectedTalent.name}</strong>
<small>{selectedTalent.description}</small>
</>
) : (
<small>No effect selected.</small>
)}
</div>
</section>
)
})}
</div>
{selectedTalent && (
<button
className="primary-button"
disabled={Boolean(lockReason(selectedTalent)) || busyTalentId === selectedTalent.id}
onClick={() => toggleEffect(selectedTalent)}
type="button"
>
{busyTalentId === selectedTalent.id
? 'Saving...'
: selectedTalent.rank > 0
? 'Remove'
: 'Activate'}
</button>
)}
</div>
<div className="effect-pool">
{visibleTalents.map((talent) => {
const reason = lockReason(talent)
const active = talent.rank > 0
const selected = selectedTalent?.id === talent.id
const isBusy = busyTalentId === talent.id
return (
<button
className={`${active ? 'active' : ''} ${selected ? 'selected' : ''}`}
disabled={Boolean(reason) || isBusy}
key={talent.id}
onClick={() => {
setSelectedTalentId(talent.id)
void toggleEffect(talent)
}}
type="button"
>
<span>{talent.glyph}</span>
<div>
<strong>{talent.name}</strong>
<small>{EFFECT_SOURCE_LABELS[effectSource(talent.effectType)] ?? 'Spell'}</small>
</div>
<i>{isBusy ? 'Saving' : active ? 'Active' : reason || 'Available'}</i>
</button>
)
})}
</div>
{effectPageCount > 1 && (
<div className="effect-pager">
<button
disabled={effectPage === 0}
onClick={() => setEffectPage((page) => Math.max(0, page - 1))}
type="button"
>
Prev
</button>
<span>{effectPage + 1}/{effectPageCount}</span>
<button
disabled={effectPage >= effectPageCount - 1}
onClick={() => setEffectPage((page) => Math.min(effectPageCount - 1, page + 1))}
type="button"
>
Next
</button>
</div>
)}
</section>
</div>
)}
<footer className="talent-footer">
<span>{message || 'Talent changes are saved immediately.'}</span>
<span>{message || 'Spell effect changes are saved immediately.'}</span>
<button
className="text-button"
disabled={classPointsSpent === 0 || resetting}
onClick={refundTree}
disabled={selectedEffects.length === 0 || resetting}
onClick={clearEffects}
type="button"
>
{resetting ? 'Refunding...' : 'Reset Tree'}
{resetting ? 'Clearing...' : 'Clear Effects'}
</button>
</footer>
</>
+193 -16
View File
@@ -11,6 +11,7 @@ import {
} from 'react'
import type { CombatLogEntry, PartyMember, Spell } from './game'
import {
getNativeDisplays,
hasNativeDualScreenBridge,
openNativeTopDisplay,
} from './nativeDualScreen'
@@ -23,6 +24,7 @@ import { ControllerBindingLabel } from './components/ControllerIcons'
const STORAGE_KEY = 'ashen-halls-dual-screen-enabled'
const SNAPSHOT_KEY = 'ashen-halls-dual-screen-snapshot'
const STARTUP_CHOICE_KEY = 'ashen-halls-dual-screen-startup-choice'
const CHANNEL_NAME = 'ashen-halls-dual-screen'
export type DualScreenCombatState = {
@@ -40,7 +42,7 @@ export type DualScreenCombatState = {
partySize: number
selectedId: string
log: CombatLogEntry[]
status: 'playing' | 'won' | 'lost' | 'part-complete' | 'upgrade-choice'
status: 'playing' | 'won' | 'lost' | 'part-complete' | 'marathon-choice' | 'upgrade-choice'
resource: number
maxResource: number
resourceName: string
@@ -51,15 +53,32 @@ export type DualScreenCombatState = {
controllerIconStyle: ControllerIconStyle
directPartyTargeting: boolean
paused: boolean
targetGroup: 0 | 1
targetGroup: 0 | 1 | 2
speedMultiplier: 1 | 2
}
export type DualScreenWorkshopState = {
mode: 'class' | 'equipment' | 'crafting' | 'talents'
title: string
subtitle: string
summary?: string
items: Array<{
glyph?: string
title: string
meta?: string
detail?: string
status?: string
}>
}
type DualScreenMessage =
| { type: 'combat-state'; state: DualScreenCombatState }
| { type: 'workshop-state'; state: DualScreenWorkshopState }
| { type: 'companion-ready' }
| { type: 'companion-heartbeat' }
| { type: 'control-action'; action: InputAction }
| { type: 'combat-ended' }
| { type: 'workshop-ended' }
type DualScreenContextValue = {
enabled: boolean
@@ -100,6 +119,13 @@ function loadRecentSnapshot() {
}
}
function memberHotEffects(member: PartyMember) {
if (member.hotEffects?.length) return member.hotEffects
return member.hotTicks > 0
? [{ id: 'legacy-renew', spellId: 'legacy-renew', label: 'Renew', ticks: member.hotTicks, power: 6 }]
: []
}
export function DualScreenProvider({ children }: { children: ReactNode }) {
const [enabled, setEnabledState] = useState(
() => localStorage.getItem(STORAGE_KEY) === 'true',
@@ -172,6 +198,73 @@ export function useDualScreen() {
return context
}
export function DualScreenStartupPrompt() {
const { openTopDisplay, setEnabled } = useDualScreen()
const [visible, setVisible] = useState(false)
const [displayCount, setDisplayCount] = useState<number | null>(null)
const [message, setMessage] = useState('')
const autoOpenedRef = useRef(false)
useEffect(() => {
if (!hasNativeDualScreenBridge()) return
if (new URLSearchParams(window.location.search).has('display')) return
const choice = localStorage.getItem(STARTUP_CHOICE_KEY)
if (choice === 'yes') {
if (autoOpenedRef.current) return
autoOpenedRef.current = true
openTopDisplay().catch(() => {
// Settings can still launch the display manually if Android rejects startup launch.
})
return
}
if (choice === 'no') return
getNativeDisplays()
.then((result) => setDisplayCount(result.displays.length))
.catch(() => setDisplayCount(null))
.finally(() => setVisible(true))
}, [openTopDisplay])
async function enableDualScreen() {
localStorage.setItem(STARTUP_CHOICE_KEY, 'yes')
setMessage('Opening second display...')
const opened = await openTopDisplay()
if (opened) {
setVisible(false)
return
}
setMessage('No second display found. Check Thor display mode, then try again.')
}
function skipDualScreen() {
localStorage.setItem(STARTUP_CHOICE_KEY, 'no')
setEnabled(false)
setVisible(false)
}
if (!visible) return null
return (
<div className="dual-startup-prompt" role="dialog" aria-modal="true">
<section>
<p className="eyebrow">Display Setup</p>
<h2>Use Dual-Screen Mode?</h2>
<p>
Choose yes on AYN Thor. The game opens the combat view on the upper
display and keeps controls on the lower display.
</p>
{displayCount !== null && (
<small>{displayCount} Android display{displayCount === 1 ? '' : 's'} detected.</small>
)}
{message && <small>{message}</small>}
<div>
<button onClick={enableDualScreen} type="button">Yes, Enable</button>
<button onClick={skipDualScreen} type="button">No</button>
</div>
</section>
</div>
)
}
export function useDualScreenPublisher(
state: DualScreenCombatState,
enabled: boolean,
@@ -211,16 +304,64 @@ export function useDualScreenPublisher(
}, [enabled, state])
}
export function useDualScreenWorkshopPublisher(
state: DualScreenWorkshopState | null,
enabled: boolean,
) {
const stateRef = useRef(state)
useEffect(() => {
stateRef.current = state
}, [state])
useEffect(() => {
if (!enabled || !state) return
const channel = createChannel()
if (!channel) return
const publish = () => {
if (stateRef.current) {
channel.postMessage({
type: 'workshop-state',
state: stateRef.current,
} satisfies DualScreenMessage)
}
}
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'companion-ready') publish()
}
publish()
return () => {
channel.postMessage({ type: 'workshop-ended' } satisfies DualScreenMessage)
channel.close()
}
}, [enabled, state])
useEffect(() => {
if (!enabled || !state) return
const channel = createChannel()
channel?.postMessage({ type: 'workshop-state', state } satisfies DualScreenMessage)
channel?.close()
}, [enabled, state])
}
export function DualScreenBottomDisplay() {
const [state, setState] = useState<DualScreenCombatState | null>(loadRecentSnapshot)
const [workshopState, setWorkshopState] = useState<DualScreenWorkshopState | null>(null)
useEffect(() => {
const channel = createChannel()
if (!channel) return
const announce = () => channel.postMessage({ type: 'companion-ready' } satisfies DualScreenMessage)
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'combat-state') setState(event.data.state)
if (event.data.type === 'combat-state') {
setState(event.data.state)
setWorkshopState(null)
}
if (event.data.type === 'workshop-state') {
setWorkshopState(event.data.state)
setState(null)
}
if (event.data.type === 'combat-ended') setState(null)
if (event.data.type === 'workshop-ended') setWorkshopState(null)
}
announce()
const timer = window.setInterval(() => {
@@ -238,6 +379,40 @@ export function DualScreenBottomDisplay() {
channel?.close()
}
if (!state && workshopState) {
return (
<main className="dual-bottom-display workshop-bottom-display">
<header className="dual-controls-header">
<div>
<p className="eyebrow">{workshopState.mode}</p>
<h1>{workshopState.title}</h1>
</div>
<div className="dual-controls-progress">
<span>{workshopState.subtitle}</span>
</div>
</header>
{workshopState.summary && (
<section className="workshop-bottom-summary">
{workshopState.summary}
</section>
)}
<section className="workshop-bottom-grid">
{workshopState.items.map((item, index) => (
<article key={`${item.title}-${index}`}>
{item.glyph && <span>{item.glyph}</span>}
<div>
<strong>{item.title}</strong>
{item.meta && <small>{item.meta}</small>}
{item.detail && <p>{item.detail}</p>}
</div>
{item.status && <i>{item.status}</i>}
</article>
))}
</section>
</main>
)
}
if (!state) {
return (
<main className="dual-bottom-display dual-bottom-waiting">
@@ -271,6 +446,7 @@ export function DualScreenBottomDisplay() {
</div>
<div className="dual-controls-mana">
<span>{state.resourceName} {Math.floor(state.resource)} / {state.maxResource}</span>
{state.speedMultiplier === 2 && <strong className="speed-badge">2x speed</strong>}
<div className="bar mana-bar">
<span style={{ width: `${(state.resource / state.maxResource) * 100}%` }} />
</div>
@@ -280,9 +456,9 @@ export function DualScreenBottomDisplay() {
<div className={`dual-controls-targets ${state.directPartyTargeting ? 'direct' : ''}`}>
{state.directPartyTargeting ? (
<>
{([1, 2, 3, 4, 5] as const).map((slot) => {
{([1, 2, 3, 4, 5, 6] as const).map((slot) => {
const action = `targetParty${slot}` as InputAction
const memberIndex = slot - 1 + (state.partySize === 10 ? state.targetGroup * 5 : 0)
const memberIndex = slot - 1 + (state.partySize > 6 ? state.targetGroup * 6 : 0)
return (
<button onClick={() => sendAction(action)} type="button" key={action}>
<ControllerBindingLabel
@@ -293,13 +469,13 @@ export function DualScreenBottomDisplay() {
</button>
)
})}
{state.partySize === 10 && (
{state.partySize > 6 && (
<button onClick={() => sendAction('toggleTargetGroup')} type="button">
<ControllerBindingLabel
binding={state.bindings.toggleTargetGroup}
iconStyle={state.controllerIconStyle}
/>{' '}
Party Group {state.targetGroup + 1}
Party Group {state.targetGroup + 1}/{Math.ceil(state.partySize / 6)}
</button>
)}
</>
@@ -396,14 +572,17 @@ export function DualScreenTopCombat({
</section>
<section className="dual-top-party">
<div className={`dual-top-party-grid ${state.partySize === 10 ? 'raid' : ''}`}>
<div className={`dual-top-party-grid ${state.partySize > 6 ? 'raid' : ''}`}>
{state.party.map((member, index) => {
const partySlot = index + 1 + (state.directPartyTargeting && state.partySize === 10 ? state.targetGroup * 5 : 0)
const partySlot = (index % 6) + 1
const targetAction = `targetParty${partySlot}` as InputAction
const targetBinding = state.directPartyTargeting ? state.bindings[targetAction] : null
const groupStart = state.partySize > 6 ? state.targetGroup * 6 : 0
const inCurrentTargetGroup = index >= groupStart && index < groupStart + 6
const targetBinding = state.directPartyTargeting && inCurrentTargetGroup ? state.bindings[targetAction] : null
return (
<button
className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
data-party-member-id={member.id}
key={member.id}
onClick={() => onSelectTarget(member.id)}
type="button"
@@ -418,6 +597,7 @@ export function DualScreenTopCombat({
{member.shield > 0 && (
<i style={{ width: `${(member.shield / member.maxHealth) * 100}%` }} />
)}
<em className="health-text">{Math.ceil(member.health)} / {member.maxHealth}</em>
</div>
{state.directPartyTargeting && targetBinding && (
<div className="member-target-key">
@@ -428,7 +608,9 @@ export function DualScreenTopCombat({
</div>
)}
<div className="member-effects">
{member.hotTicks > 0 && <span className="buff">Renew</span>}
{memberHotEffects(member).map((effect) => (
<span className="buff" key={effect.id}>{effect.label}</span>
))}
{member.debuff && <span className="debuff">{member.debuff}</span>}
</div>
</button>
@@ -437,11 +619,6 @@ export function DualScreenTopCombat({
</div>
</section>
<footer className="dual-top-log">
{state.log.slice(0, 3).map((entry) => (
<span className={entry.tone} key={entry.id}>{entry.text}</span>
))}
</footer>
</div>
)
}
+54 -2
View File
@@ -8,6 +8,20 @@ export type PartyMember = {
maxHealth: number
shield: number
hotTicks: number
hotEffects?: Array<{
id: string
spellId: string
label: string
ticks: number
power: number
}>
bounceHeals?: Array<{
id: string
label: string
charges: number
power: number
}>
damageReductionTicks?: number
debuff?: string
debuffTicks?: number
poisonStacks?: number
@@ -24,7 +38,8 @@ export type Spell = {
cooldown: number
power: number
glyph: string
kind: 'direct' | 'hot' | 'group' | 'shield' | 'cleanse'
kind: 'direct' | 'hot' | 'group' | 'shield' | 'cleanse' | 'damage_reduction' | 'bounce_heal'
effectType?: string
}
export type Encounter = {
@@ -44,12 +59,16 @@ export type CombatLogEntry = {
tone: 'system' | 'heal' | 'danger' | 'loot'
}
export const TANKLESS_DAMAGE_MULTIPLIER = 1.35
export const DEFAULT_GROUP_HEAL_TARGETS = 4
export const INITIAL_PARTY: PartyMember[] = [
{ id: 'brann', name: 'Brann', role: 'Tank', health: 150, maxHealth: 150, shield: 0, hotTicks: 0 },
{ id: 'mira', name: 'Mira', role: 'Healer', health: 100, maxHealth: 100, shield: 0, hotTicks: 0 },
{ id: 'kael', name: 'Kael', role: 'Damage', health: 105, maxHealth: 105, shield: 0, hotTicks: 0 },
{ id: 'ves', name: 'Ves', role: 'Damage', health: 95, maxHealth: 95, shield: 0, hotTicks: 0 },
{ id: 'orin', name: 'Orin', role: 'Damage', health: 110, maxHealth: 110, shield: 0, hotTicks: 0 },
{ id: 'lyra', name: 'Lyra', role: 'Damage', health: 98, maxHealth: 98, shield: 0, hotTicks: 0 },
]
export const RAID_PARTY: PartyMember[] = [
@@ -63,6 +82,14 @@ export const RAID_PARTY: PartyMember[] = [
{ id: 'lyra', name: 'Lyra', role: 'Damage', health: 98, maxHealth: 98, shield: 0, hotTicks: 0 },
{ id: 'dax', name: 'Dax', role: 'Damage', health: 108, maxHealth: 108, shield: 0, hotTicks: 0 },
{ id: 'nyx', name: 'Nyx', role: 'Damage', health: 100, maxHealth: 100, shield: 0, hotTicks: 0 },
{ id: 'ren', name: 'Ren', role: 'Damage', health: 104, maxHealth: 104, shield: 0, hotTicks: 0 },
{ id: 'iona', name: 'Iona', role: 'Damage', health: 96, maxHealth: 96, shield: 0, hotTicks: 0 },
{ id: 'sol', name: 'Sol', role: 'Damage', health: 106, maxHealth: 106, shield: 0, hotTicks: 0 },
{ id: 'nari', name: 'Nari', role: 'Damage', health: 99, maxHealth: 99, shield: 0, hotTicks: 0 },
{ id: 'joss', name: 'Joss', role: 'Damage', health: 103, maxHealth: 103, shield: 0, hotTicks: 0 },
{ id: 'eira', name: 'Eira', role: 'Damage', health: 97, maxHealth: 97, shield: 0, hotTicks: 0 },
{ id: 'cato', name: 'Cato', role: 'Damage', health: 107, maxHealth: 107, shield: 0, hotTicks: 0 },
{ id: 'rhea', name: 'Rhea', role: 'Damage', health: 101, maxHealth: 101, shield: 0, hotTicks: 0 },
]
export const SPELLS: Spell[] = [
@@ -92,7 +119,7 @@ export const SPELLS: Spell[] = [
id: 'radiance',
key: '3',
name: 'Radiance',
description: 'Restores health to every living party member.',
description: 'Restores health to up to 4 injured party members.',
cost: 12,
cooldown: 8,
power: 18,
@@ -155,3 +182,28 @@ export const ENCOUNTERS: Encounter[] = [
isBoss: true,
},
]
export function partyDamageOutput(party: PartyMember[], baseDamage: number) {
const livingCount = party.filter((member) => member.health > 0).length
return Math.round(baseDamage * (livingCount / Math.max(1, party.length)))
}
export function tankPressureTargets(party: PartyMember[]) {
const living = party.filter((member) => member.health > 0)
const tanks = living.filter((member) => member.role === 'Tank')
if (tanks.length > 0) return { targets: tanks, multiplier: 1 }
const damageDealer = living
.filter((member) => member.role === 'Damage')
.sort((left, right) => right.health - left.health)[0]
return {
targets: damageDealer ? [damageDealer] : [],
multiplier: TANKLESS_DAMAGE_MULTIPLIER,
}
}
export function groupHealTargets(party: PartyMember[], targetCount = DEFAULT_GROUP_HEAL_TARGETS) {
return party
.filter((member) => member.health > 0)
.sort((left, right) => (left.health / left.maxHealth) - (right.health / right.maxHealth))
.slice(0, targetCount)
}
+853 -198
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -12,4 +12,10 @@ body {
margin: 0;
min-width: 320px;
min-height: 100vh;
overflow: hidden;
}
#root {
height: 100dvh;
overflow: hidden;
}
+70 -10
View File
@@ -33,7 +33,9 @@ export const INPUT_ACTIONS = [
'targetParty3',
'targetParty4',
'targetParty5',
'targetParty6',
'toggleTargetGroup',
'toggleSpeed',
'pause',
] as const
@@ -60,7 +62,9 @@ export const ACTION_LABELS: Record<InputAction, string> = {
targetParty3: 'Target Party Member 3',
targetParty4: 'Target Party Member 4',
targetParty5: 'Target Party Member 5',
targetParty6: 'Target Party Member 6',
toggleTargetGroup: 'Switch Raid Target Group',
toggleSpeed: 'Toggle 2x Speed',
pause: 'Pause Menu',
}
@@ -85,7 +89,9 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
targetParty3: 'F3',
targetParty4: 'F4',
targetParty5: 'F5',
targetParty6: 'F6',
toggleTargetGroup: 'Tab',
toggleSpeed: 'Backquote',
pause: 'Escape',
},
controller: {
@@ -108,7 +114,9 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
targetParty3: 'Button15',
targetParty4: 'Button13',
targetParty5: 'Button4',
targetParty6: 'Button10',
toggleTargetGroup: 'Button6',
toggleSpeed: 'Button11',
pause: 'Button9',
},
}
@@ -141,7 +149,8 @@ const InputContext = createContext<InputContextValue | null>(null)
function loadBindings(): Record<InputDevice, InputBindings> {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Partial<Record<InputDevice, Partial<InputBindings>>>
const controller = { ...DEFAULT_BINDINGS.controller, ...saved.controller }
const savedController = saved.controller
const controller = { ...DEFAULT_BINDINGS.controller, ...savedController }
const usesLegacyAbilityDefaults = [
'Button2',
'Button3',
@@ -162,6 +171,15 @@ function loadBindings(): Record<InputDevice, InputBindings> {
ability6: DEFAULT_BINDINGS.controller.ability6,
})
}
if (savedController?.toggleSpeed === 'Button7') {
controller.toggleSpeed = DEFAULT_BINDINGS.controller.toggleSpeed
}
if (savedController?.ability6 === 'Button10') {
controller.ability6 = DEFAULT_BINDINGS.controller.ability6
}
if (savedController?.targetParty6 === 'Button11') {
controller.targetParty6 = DEFAULT_BINDINGS.controller.targetParty6
}
return {
pc: { ...DEFAULT_BINDINGS.pc, ...saved.pc },
controller,
@@ -202,13 +220,19 @@ function bindingGroup(action: InputAction) {
}
function isVisible(element: HTMLElement) {
if (element.hidden || element.getAttribute('aria-hidden') === 'true') return false
return element.getClientRects().length > 0
}
function focusableElements() {
const keyboard = document.querySelector<HTMLElement>('.controller-keyboard')
const pauseMenu = document.querySelector<HTMLElement>('.pause-screen')
const scope: ParentNode = keyboard ?? pauseMenu ?? document
const dialog = Array.from(
document.querySelectorAll<HTMLElement>(
'.result-screen, .binding-capture, .dual-startup-prompt',
),
).find(isVisible)
const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
return Array.from(
scope.querySelectorAll<HTMLElement>(
'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
@@ -256,7 +280,14 @@ function moveFocus(action: InputAction) {
const next = ranked[0]?.candidate
if (!next) return
next.focus({ preventScroll: true })
next.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}
function hasUiOverlay() {
return Array.from(
document.querySelectorAll<HTMLElement>(
'.pause-screen, .result-screen, .binding-capture, .dual-startup-prompt, .controller-keyboard',
),
).some(isVisible)
}
const BUTTON_LABELS: Record<number, string> = {
@@ -416,18 +447,24 @@ export function InputProvider({ children }: { children: ReactNode }) {
}, [])
const dispatchAction = useCallback((action: InputAction, device: InputDevice) => {
setLastDevice(device)
document.documentElement.dataset.inputDevice = device
const uiOverlay = hasUiOverlay()
const combatActive = Boolean(document.querySelector('[data-combat-active="true"]'))
setLastDevice(device)
document.documentElement.dataset.inputDevice = device
if (action.startsWith('navigate')) {
if (!combatActive) moveFocus(action)
if (uiOverlay || !combatActive) moveFocus(action)
} else if (action === 'confirm') {
const active = document.activeElement
if (isTextInput(active)) {
setKeyboardInput(active)
window.requestAnimationFrame(() => focusFirstControl())
} else if (active instanceof HTMLElement && active.matches('button, [role="button"]')) {
} else if (
active instanceof HTMLElement
&& active.matches('button:not(:disabled), [role="button"]')
&& isVisible(active)
) {
active.click()
} else {
focusFirstControl()
@@ -435,7 +472,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
} else if (action === 'back') {
if (keyboardInputRef.current) {
closeKeyboard()
} else if (!combatActive) {
} else if (uiOverlay || !combatActive) {
const backButton = Array.from(
document.querySelectorAll<HTMLButtonElement>('.back-button:not(:disabled)'),
).find(isVisible)
@@ -458,22 +495,34 @@ export function InputProvider({ children }: { children: ReactNode }) {
const combatActive = Boolean(
document.querySelector('[data-combat-active="true"]'),
)
const uiOverlay = hasUiOverlay()
const menuDpadActions: Partial<Record<string, InputAction>> = {
Button12: 'navigateUp',
Button13: 'navigateDown',
Button14: 'navigateLeft',
Button15: 'navigateRight',
}
const uiPriority = [
'navigateUp',
'navigateDown',
'navigateLeft',
'navigateRight',
'confirm',
'back',
] satisfies InputAction[]
const directTargetActions = [
'targetParty1',
'targetParty2',
'targetParty3',
'targetParty4',
'targetParty5',
'targetParty6',
'toggleTargetGroup',
'toggleSpeed',
] satisfies InputAction[]
const combatPriority = [
'pause',
'toggleSpeed',
'ability1',
'ability2',
'ability3',
@@ -487,7 +536,11 @@ export function InputProvider({ children }: { children: ReactNode }) {
'navigateLeft',
'navigateRight',
] satisfies InputAction[]
const action = combatActive && preferencesRef.current.directPartyTargeting
const action = menuDpadActions[token] && (!combatActive || uiOverlay)
? menuDpadActions[token]
: uiOverlay
? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token)
: combatActive && preferencesRef.current.directPartyTargeting
? [...directTargetActions, ...combatPriority].find(
(candidate) => bindingsRef.current.controller[candidate] === token,
)
@@ -541,8 +594,13 @@ export function InputProvider({ children }: { children: ReactNode }) {
const ensureFocus = () => {
const combatActive = document.querySelector('[data-combat-active="true"]')
if (combatActive) return
const candidates = focusableElements()
const active = document.activeElement
const activeIsUsable = active instanceof HTMLElement
&& candidates.includes(active)
&& isVisible(active)
if (
document.activeElement === document.body
(!activeIsUsable || document.activeElement === document.body)
&& !keyboardInputRef.current
&& !captureRef.current
) {
@@ -553,6 +611,8 @@ export function InputProvider({ children }: { children: ReactNode }) {
window.requestAnimationFrame(ensureFocus)
})
observer.observe(document.getElementById('root') ?? document.body, {
attributes: true,
attributeFilter: ['aria-hidden', 'class', 'disabled', 'hidden', 'style'],
childList: true,
subtree: true,
})
+16 -2
View File
@@ -1,9 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Capacitor } from '@capacitor/core'
import './index.css'
import App from './App.tsx'
import { InputProvider } from './input.tsx'
import { DualScreenBottomDisplay, DualScreenProvider } from './dualScreen.tsx'
import { DualScreenBottomDisplay, DualScreenProvider, DualScreenStartupPrompt } from './dualScreen.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
@@ -11,6 +12,7 @@ createRoot(document.getElementById('root')!).render(
<DualScreenBottomDisplay />
) : (
<DualScreenProvider>
<DualScreenStartupPrompt />
<InputProvider>
<App />
</InputProvider>
@@ -19,7 +21,19 @@ createRoot(document.getElementById('root')!).render(
</StrictMode>,
)
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
const isNativeApp = Capacitor.isNativePlatform()
if (import.meta.env.PROD && isNativeApp && 'serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations()
.then((registrations) => Promise.all(registrations.map((registration) => registration.unregister())))
.then(() => caches.keys())
.then((keys) => Promise.all(keys.filter((key) => key.startsWith('chronicle-')).map((key) => caches.delete(key))))
.catch(() => {
// Native app assets should come directly from the APK when cache cleanup is unavailable.
})
}
if (import.meta.env.PROD && !isNativeApp && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').catch(() => {
// Offline launch remains optional when registration is unavailable.
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -59,7 +59,7 @@ export type Item = {
slug: string
name: string
slot: EquipmentSlot
rarity: 'common' | 'uncommon' | 'rare' | 'epic'
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
itemLevel: number
healingPower: number
maxResourceBonus: number
@@ -134,6 +134,11 @@ export type CraftingRecipe = {
canCraft: boolean
}
export type GearUpgradePath = {
fromItemId: number
toItemId: number
}
export type LootRollItem = Omit<Item, 'quantity' | 'equipped'> & {
quantity: number
duplicate: boolean
@@ -163,11 +168,13 @@ export type Dungeon = {
partySize: number
completionItemLevel: number | null
experienceReward: number
imageUrl: string
description: string
locationName: string
difficulties: Difficulty[]
encounters: DungeonEncounter[]
completionLoot: Array<Omit<Item, 'quantity' | 'equipped'>>
completionCount?: number
leaderboard: LeaderboardEntry[]
leaderboards: {
part_1: LeaderboardEntry[]
@@ -223,6 +230,7 @@ export type CharacterProfile = {
}
setBonuses: SetBonus[]
craftingRecipes: CraftingRecipe[]
gearUpgradePaths: GearUpgradePath[]
dungeons: Dungeon[]
}
@@ -234,6 +242,7 @@ export type Account = {
export type AuthSession = {
account: Account | null
profile: CharacterProfile | null
token?: string
}
export type BonusItem = {
@@ -247,6 +256,7 @@ export type BonusItem = {
maxResourceBonus: number
glyph: string
description: string
quantity: number
duplicate: boolean
quantityAfter: number
}
@@ -317,6 +327,7 @@ export async function completeDungeon(
completedPart?: number,
startPart?: number,
partDurationSeconds?: [number, number, number],
hardMode?: boolean,
): Promise<DungeonReward> {
return activeGameRepository().completeDungeon(
dungeonId,
@@ -326,6 +337,7 @@ export async function completeDungeon(
completedPart,
startPart,
partDurationSeconds,
hardMode,
)
}
@@ -338,6 +350,8 @@ export async function completeRoguelike(
options?: {
bossesCleared?: number
experienceMode?: 'default' | 'pvp-boss-quarter-level'
lootSourceEncounterId?: number
roguelikeStage?: number
},
): Promise<DungeonReward> {
return activeGameRepository().completeRoguelike(
@@ -374,6 +388,10 @@ export async function craftItem(recipeId: number): Promise<CharacterProfile> {
return activeGameRepository().craftItem(recipeId)
}
export async function upgradeItem(itemId: number): Promise<CharacterProfile> {
return activeGameRepository().upgradeItem(itemId)
}
export async function rollEncounterLoot(
encounterId: number,
difficultyId: number,
+22
View File
@@ -12,6 +12,7 @@ export type CpuPvpLeaderboardEntry = {
}
const cpuLeaderboardKey = 'chronicle.pvpCpuLeaderboard.v1'
const checkpointKey = 'chronicle.pvpRoguelikeCheckpoint.v1'
export function randomCpuDifficulty(): CpuDifficulty {
return (Math.floor(Math.random() * 5) + 1) as CpuDifficulty
@@ -44,3 +45,24 @@ export function recordCpuPvpLeaderboard(entry: CpuPvpLeaderboardEntry) {
.slice(0, 30)
localStorage.setItem(cpuLeaderboardKey, JSON.stringify(next))
}
function checkpointStorageKey(characterId: number, contentType: PvpContentType) {
return `${checkpointKey}:${characterId}:${contentType}`
}
export function loadPvpRoguelikeCheckpoint(characterId: number, contentType: PvpContentType) {
const value = Number(localStorage.getItem(checkpointStorageKey(characterId, contentType)) ?? 1)
return Number.isInteger(value) && value >= 5 ? value : 1
}
export function recordPvpRoguelikeCheckpoint(
characterId: number,
contentType: PvpContentType,
stage: number,
) {
if (stage < 5 || stage % 5 !== 0) return loadPvpRoguelikeCheckpoint(characterId, contentType)
const current = loadPvpRoguelikeCheckpoint(characterId, contentType)
const next = Math.max(current, stage)
localStorage.setItem(checkpointStorageKey(characterId, contentType), String(next))
return next
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"files": ["vite.config.ts"]
}