Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1e2c6d8b5 | |||
| f7b041f86f | |||
| 05bd70a9fe | |||
| bb5c7e6e21 | |||
| 14bec979e6 | |||
| 4b45483ac3 | |||
| 6e10b37f8e | |||
| 5aac39c6c9 | |||
| 224249e372 | |||
| 66f5af4484 | |||
| 8f5a957963 | |||
| f8a1fbc5e2 | |||
| bab2dce6c3 | |||
| cb38042eca | |||
| 753bba581a | |||
| 2300973164 | |||
| 1281be69d8 | |||
| 4fc15ebe9a | |||
| 7313c968e6 | |||
| 207fcd1a15 | |||
| fd6a1ce3c7 | |||
| f8b98e6b23 | |||
| fc7c6488ea | |||
| ba6d3b614e | |||
| 88874933c3 | |||
| bf12aefeeb | |||
| 814eb1998d |
@@ -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.
|
||||
@@ -2,5 +2,8 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -138,9 +138,12 @@ services:
|
||||
|
||||
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`. The startup command installs
|
||||
dependencies, applies schema migrations, builds the web app, and starts the
|
||||
production server.
|
||||
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:
|
||||
|
||||
@@ -220,11 +223,22 @@ 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
|
||||
@@ -236,10 +250,14 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.warren.iwanttoheal"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 41
|
||||
versionName "1.0.24"
|
||||
versionCode 69
|
||||
versionName "1.0.50"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 = 125;
|
||||
private static final long DPAD_THROTTLE_MS = 220;
|
||||
private long lastDpadDispatchAt = 0;
|
||||
|
||||
@Override
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,674 @@
|
||||
-- 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, 383002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 683002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 383002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 683002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 383002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 683002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 483002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 783002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 483002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 783002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 483002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 783002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 583002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 583002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 583002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 683003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 983003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 683003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 983003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 683003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 983003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 783003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 1083003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1205, 783003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1205, 1083003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1206, 783003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1206, 1083003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1207, 883003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1207, 1183003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1208, 883003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1208, 1183003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1209, 883003, 7);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1209, 1183003, 8);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 983004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 1283004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 983004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 1283004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 983004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 1283004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1304, 1083004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1304, 1383004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1305, 1083004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1305, 1383004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1306, 1083004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1306, 1383004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1307, 1183004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1307, 1483004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1308, 1183004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1308, 1483004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1309, 1183004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1309, 1483004, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1401, 1283005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1401, 1583005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1402, 1283005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1402, 1583005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1403, 1283005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1403, 1583005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1404, 1383005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1404, 1683005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1405, 1383005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1405, 1683005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1406, 1383005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1406, 1683005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1407, 1483005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1407, 1783005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1408, 1483005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1408, 1783005, 13);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1409, 1483005, 12);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1409, 1783005, 13);
|
||||
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;
|
||||
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS dungeons (
|
||||
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,
|
||||
|
||||
@@ -1,172 +1,139 @@
|
||||
# Gearing System
|
||||
|
||||
## Goal
|
||||
## Current Rule
|
||||
|
||||
Gearing should move from boss-specific multi-item drop tables to one clear currency loop:
|
||||
The game uses fewer playable content tiers and more gear upgrade steps.
|
||||
|
||||
1. Kill bosses.
|
||||
2. Earn boss coins.
|
||||
3. Craft gear with those coins.
|
||||
4. Upgrade that boss gear into the next item-level tier with higher-rarity coins.
|
||||
Content tiers:
|
||||
|
||||
This keeps boss loot readable, removes low-percentage frustration, and makes every boss kill progress a targeted gear goal.
|
||||
| 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 source and an item-level tier.
|
||||
Coins are component items. Each coin is tied to a boss and a content tier.
|
||||
|
||||
| Item Level | Display Color | Rarity Key | Example |
|
||||
| --- | --- | --- | --- |
|
||||
| 5 | White | common | Bulldrome Coin |
|
||||
| Content Tier | Coin Prefix | Rarity Key | Example |
|
||||
| ---: | --- | --- | --- |
|
||||
| 1 | Raw | common | Raw Bulldrome Coin |
|
||||
| 10 | Green | uncommon | Green Bulldrome Coin |
|
||||
| 15 | Blue | rare | Blue Bulldrome Coin |
|
||||
| 20 | Purple | epic | Purple Bulldrome Coin |
|
||||
| 25 | Orange | legendary | Orange Bulldrome Coin |
|
||||
|
||||
Implementation note: the current TypeScript rarity union supports `common`, `uncommon`, `rare`, and `epic`. Orange needs a new rarity key, recommended as `legendary`, plus UI color styling.
|
||||
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 has one loot roll.
|
||||
|
||||
For now, each successful boss loot roll awards 1 to 3 coins:
|
||||
|
||||
| Roll Result | Coins Awarded |
|
||||
| --- | --- |
|
||||
| Low roll | 1 coin |
|
||||
| Normal roll | 2 coins |
|
||||
| High roll | 3 coins |
|
||||
|
||||
Recommended weighting:
|
||||
|
||||
| Coins | Chance |
|
||||
| --- | --- |
|
||||
| 1 | 50% |
|
||||
| 2 | 35% |
|
||||
| 3 | 15% |
|
||||
|
||||
The coin source comes from the defeated boss. Bulldrome drops Bulldrome coins, Rathian drops Rathian coins, and so on.
|
||||
|
||||
The coin tier comes from content difficulty or roguelike depth:
|
||||
|
||||
| Source | Coin Tier |
|
||||
| --- | --- |
|
||||
| Item level 5 content | White level 5 coins |
|
||||
| Item level 10 content | Green level 10 coins |
|
||||
| Item level 15 content | Blue level 15 coins |
|
||||
| Item level 20 content | Purple level 20 coins |
|
||||
| Item level 25 content | Orange level 25 coins |
|
||||
|
||||
## Crafting Costs
|
||||
|
||||
Gear is crafted with boss coins from the same boss and item-level tier.
|
||||
|
||||
| Gear Item Level | Cost |
|
||||
| --- | --- |
|
||||
| 5 | 5 white boss coins |
|
||||
| 10 | 10 green boss coins |
|
||||
| 15 | 15 blue boss coins |
|
||||
| 20 | 20 purple boss coins |
|
||||
| 25 | 25 orange boss coins |
|
||||
|
||||
Example:
|
||||
|
||||
- Bulldrome item-level 5 helmet costs 5 white Bulldrome coins.
|
||||
- Bulldrome item-level 10 helmet costs 10 green Bulldrome coins.
|
||||
- Rathian item-level 20 gloves cost 20 purple Rathian coins.
|
||||
|
||||
## Gear Upgrades
|
||||
|
||||
Crafting can create gear directly, but upgrades should become the preferred long-term path.
|
||||
|
||||
Upgrade rule:
|
||||
|
||||
- Existing boss gear upgrades into the next item-level version of the same boss gear.
|
||||
- Upgrade cost uses coins from the next tier.
|
||||
- Required coin quantity equals the target item level.
|
||||
Each boss drops one boss coin for the selected content tier.
|
||||
|
||||
Examples:
|
||||
|
||||
| Upgrade | Cost |
|
||||
| --- | --- |
|
||||
| Bulldrome item level 5 gear -> Bulldrome item level 10 gear | 10 green Bulldrome coins |
|
||||
| Bulldrome item level 10 gear -> Bulldrome item level 15 gear | 15 blue Bulldrome coins |
|
||||
| Bulldrome item level 15 gear -> Bulldrome item level 20 gear | 20 purple Bulldrome coins |
|
||||
| Bulldrome item level 20 gear -> Bulldrome item level 25 gear | 25 orange Bulldrome coins |
|
||||
- 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.
|
||||
|
||||
Upgrade should consume the old item and award the upgraded item. This avoids duplicate clutter and keeps equipment identity clear.
|
||||
## 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 bosses should award coins when defeated, using the same 1 to 3 coin roll.
|
||||
Roguelike gear should follow the same tier brackets.
|
||||
|
||||
Roguelike coin tier should scale by wave band:
|
||||
Recommended mapping:
|
||||
|
||||
| Waves | Coin Tier |
|
||||
| --- | --- |
|
||||
| 1-4 | Level 5 white coins |
|
||||
| 5-9 | Level 10 green coins |
|
||||
| 10-14 | Level 15 blue coins |
|
||||
| 15-19 | Level 20 purple coins |
|
||||
| 20+ | Level 25 orange coins |
|
||||
|
||||
Boss identity can be handled two ways:
|
||||
|
||||
1. Boss-based coins: use the actual boss template selected for that roguelike boss.
|
||||
2. Roguelike coins: use a generic roguelike coin per tier.
|
||||
|
||||
Recommended first pass: boss-based coins. It reuses the same crafting economy as dungeons and makes roguelike runs feel connected to the main gear chase.
|
||||
|
||||
## Roguelike Checkpoints
|
||||
|
||||
Checkpoints should unlock every 5 waves.
|
||||
|
||||
| Highest Cleared Wave | Future Start Wave |
|
||||
| --- | --- |
|
||||
| 0-4 | 1 |
|
||||
| 5-9 | 5 |
|
||||
| Stage Band | Coin Tier |
|
||||
| --- | ---: |
|
||||
| 1-4 | 1 |
|
||||
| 5-9 | 10 |
|
||||
| 10-14 | 10 |
|
||||
| 15-19 | 15 |
|
||||
| 20+ | Highest unlocked 5-wave checkpoint |
|
||||
| 15-19 | 20 |
|
||||
| 20+ | 25 |
|
||||
|
||||
Checkpoint rule:
|
||||
Boss-based coins are still preferred. A roguelike boss should award coins for its actual boss template when possible.
|
||||
|
||||
- Unlock a checkpoint after clearing its boss band.
|
||||
- Starting from a checkpoint begins at that wave band with matching coin tier.
|
||||
- Runs should still record leaderboard progress from the selected start wave so full runs and checkpoint runs can be ranked separately later.
|
||||
## Data Notes
|
||||
|
||||
Current implementation note: the roguelike screen always starts at stage 1 and only awards XP per boss. Checkpoints need saved character progress and a start-wave selector.
|
||||
Authoritative gearing data lives in SQLite seed data:
|
||||
|
||||
## Current Code Fit
|
||||
- `db/seed.sql`
|
||||
- `src/offline-starter-profile.json`
|
||||
|
||||
The existing system already has most of the required foundation:
|
||||
Run this after changing seed data:
|
||||
|
||||
- `items.slot = 'component'` can represent coins.
|
||||
- `character_inventory.quantity` already stacks components.
|
||||
- `crafting_recipes` and `crafting_recipe_components` already support coin costs.
|
||||
- `encounter_loot_rolls` and `encounter_loot_roll_items` already persist retry-safe loot awards.
|
||||
- `completeRoguelike` is already called after each roguelike boss kill for XP, so coin awards can attach to that same flow.
|
||||
```sh
|
||||
npm run db:init
|
||||
npm run offline:export
|
||||
```
|
||||
|
||||
Needed changes:
|
||||
`npm run build` already runs `offline:export`, so a production build also refreshes the offline starter profile.
|
||||
|
||||
- Replace current 4-component boss drop tables with one boss coin per boss per tier.
|
||||
- Change boss loot roll count from multiple chance slots to one 1-3 coin roll.
|
||||
- Add orange/legendary rarity support.
|
||||
- Add upgrade recipes or a dedicated upgrade endpoint.
|
||||
- Add roguelike boss coin awards.
|
||||
- Add roguelike checkpoint persistence and start-wave selection.
|
||||
- Export updated offline starter data after seed changes.
|
||||
|
||||
## Suggestions
|
||||
|
||||
Use guaranteed coin drops for now. One to three coins per boss gives steady progress and makes craft timing easy to understand.
|
||||
|
||||
Keep coins boss-specific, not slot-specific. Slot-specific components add complexity without much decision value.
|
||||
|
||||
Use upgrade-first UI. Show the next upgrade for equipped gear before showing the full crafting catalog.
|
||||
|
||||
Keep direct crafting and upgrading at the same coin cost for the target tier. Direct crafting helps new slots catch up; upgrading preserves boss gear identity.
|
||||
|
||||
Add a pity floor only if needed later. If boss kills always award coins, the system already has deterministic progress.
|
||||
|
||||
Use one orange rarity key: `legendary`. Avoid storing display color names as rarity values; colors can change without data migration.
|
||||
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`.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -1,5 +1,6 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { catalogPayload } from '../server/catalog.mjs'
|
||||
import { getProfile } from '../server/game-api.mjs'
|
||||
|
||||
const database = new DatabaseSync(':memory:')
|
||||
@@ -7,10 +8,14 @@ const database = new DatabaseSync(':memory:')
|
||||
try {
|
||||
database.exec(readFileSync('db/schema.sql', 'utf8'))
|
||||
database.exec(readFileSync('db/seed.sql', 'utf8'))
|
||||
const profile = getProfile(database, 1)
|
||||
const catalog = catalogPayload(getProfile(database, 1))
|
||||
writeFileSync(
|
||||
'src/offline-starter-profile.json',
|
||||
`${JSON.stringify(profile, null, 2)}\n`,
|
||||
`${JSON.stringify(catalog.profile, null, 2)}\n`,
|
||||
)
|
||||
writeFileSync(
|
||||
'src/offline-catalog-meta.ts',
|
||||
`export const bundledCatalogHash = '${catalog.hash}'\n`,
|
||||
)
|
||||
console.log('Offline starter profile exported from SQLite.')
|
||||
} finally {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
function normalizeRecipe(recipe) {
|
||||
return {
|
||||
...recipe,
|
||||
components: recipe.components.map((component) => ({
|
||||
...component,
|
||||
owned: 0,
|
||||
})),
|
||||
canCraft: false,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDungeon(dungeon) {
|
||||
return {
|
||||
...dungeon,
|
||||
completionCount: 0,
|
||||
leaderboard: [],
|
||||
leaderboards: {
|
||||
part_1: [],
|
||||
part_2: [],
|
||||
part_3: [],
|
||||
full_run: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCatalogProfile(profile) {
|
||||
return {
|
||||
...profile,
|
||||
character: {
|
||||
...profile.character,
|
||||
id: 1,
|
||||
name: 'Mira',
|
||||
level: 1,
|
||||
experience: 0,
|
||||
talentPoints: 1,
|
||||
currentLevelExperience: 0,
|
||||
nextLevelExperience: 100,
|
||||
},
|
||||
abilitySlots: profile.abilitySlots,
|
||||
allocatedTalentPoints: 0,
|
||||
inventory: [],
|
||||
completedDungeonParts: 0,
|
||||
completedRaidPhases: 0,
|
||||
gearStats: {
|
||||
averageItemLevel: 0,
|
||||
healingPower: 0,
|
||||
maxResourceBonus: 0,
|
||||
},
|
||||
setBonuses: profile.setBonuses.map((bonus) => ({
|
||||
...bonus,
|
||||
equippedPieces: 0,
|
||||
active: false,
|
||||
})),
|
||||
craftingRecipes: profile.craftingRecipes.map(normalizeRecipe),
|
||||
dungeons: profile.dungeons.map(normalizeDungeon),
|
||||
}
|
||||
}
|
||||
|
||||
export function catalogHash(profile) {
|
||||
return createHash('sha256')
|
||||
.update(JSON.stringify(profile))
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
export function catalogPayload(profile) {
|
||||
const normalized = normalizeCatalogProfile(profile)
|
||||
return {
|
||||
version: 1,
|
||||
hash: catalogHash(normalized),
|
||||
profile: normalized,
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { isIP } from 'node:net'
|
||||
import { extname, resolve, sep } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { catalogPayload } from './catalog.mjs'
|
||||
|
||||
const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url))
|
||||
const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url))
|
||||
@@ -22,6 +23,7 @@ const bossImageContentTypes = {
|
||||
}
|
||||
const equipmentSlots = ['weapon', 'helmet', 'chest', 'gloves', 'boots', 'pants', 'ring', 'necklace', 'trinket']
|
||||
const componentSlot = 'component'
|
||||
const directCraftItemLevels = new Set([1, 10, 20, 25])
|
||||
const sessionCookieName = 'chronicle_session'
|
||||
const sessionLifetimeSeconds = 60 * 60 * 24 * 30
|
||||
const rateLimitBuckets = new Map()
|
||||
@@ -232,6 +234,25 @@ function consumeRateLimit(key, limit, windowMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function catchUpExperienceReward(database, accountId, characterId, baseReward, currentExperience, currentLevel) {
|
||||
const targetLevel = database.prepare(`
|
||||
SELECT COALESCE(MAX(level), 0) AS level
|
||||
FROM characters
|
||||
WHERE account_id = ?
|
||||
AND id != ?
|
||||
`).get(accountId, characterId).level
|
||||
if (targetLevel <= currentLevel) return baseReward
|
||||
const targetExperience = database.prepare(`
|
||||
SELECT experience_required AS experienceRequired
|
||||
FROM level_progression
|
||||
WHERE level = ?
|
||||
`).get(targetLevel)?.experienceRequired ?? currentExperience
|
||||
const gap = Math.max(0, targetExperience - currentExperience)
|
||||
if (gap <= 0) return baseReward
|
||||
const doubledBase = Math.min(baseReward, Math.ceil(gap / 2))
|
||||
return doubledBase * 2 + (baseReward - doubledBase)
|
||||
}
|
||||
|
||||
function normalizeUsername(value) {
|
||||
const username = String(value ?? '').trim()
|
||||
if (!/^[A-Za-z0-9_]{3,20}$/.test(username)) {
|
||||
@@ -363,13 +384,6 @@ function initializeCharacter(database, accountId, characterName, classId) {
|
||||
;[...starterSpells, null].slice(0, 6).forEach((spellId, index) => {
|
||||
insertSlot.run(characterId, index + 1, spellId)
|
||||
})
|
||||
const insertItem = database.prepare(`
|
||||
INSERT INTO character_inventory (character_id, item_id, quantity, equipped)
|
||||
VALUES (?, ?, 1, ?)
|
||||
`)
|
||||
for (let itemId = 100; itemId <= 107; itemId += 1) {
|
||||
insertItem.run(characterId, itemId, itemId === 107 ? 0 : 1)
|
||||
}
|
||||
return characterId
|
||||
}
|
||||
|
||||
@@ -577,6 +591,7 @@ export function getProfile(database, characterId, accountId) {
|
||||
dungeons.party_size AS partySize,
|
||||
dungeons.completion_item_level AS completionItemLevel,
|
||||
dungeons.experience_reward AS experienceReward,
|
||||
dungeons.image_url AS imageUrl,
|
||||
dungeons.description,
|
||||
locations.name AS locationName
|
||||
FROM dungeons
|
||||
@@ -735,6 +750,11 @@ export function getProfile(database, characterId, accountId) {
|
||||
...bonus,
|
||||
active: bonus.equippedPieces >= bonus.requiredPieces,
|
||||
}))
|
||||
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 leaderboardRuns = database.prepare(`
|
||||
SELECT
|
||||
rank,
|
||||
@@ -777,6 +797,15 @@ export function getProfile(database, characterId, accountId) {
|
||||
WHERE rank <= 10
|
||||
ORDER BY dungeonId, difficultyId, startPart, completedParts, rank
|
||||
`).all()
|
||||
const dungeonCompletionCounts = new Map(database.prepare(`
|
||||
SELECT dungeon_id AS dungeonId, COUNT(*) AS count
|
||||
FROM dungeon_runs
|
||||
WHERE character_id = ?
|
||||
AND result = 'victory'
|
||||
AND start_part = 1
|
||||
AND completed_parts >= 1
|
||||
GROUP BY dungeon_id
|
||||
`).all(characterId).map((row) => [row.dungeonId, row.count]))
|
||||
|
||||
const settings = Object.fromEntries(
|
||||
database.prepare('SELECT key, value FROM game_settings').all()
|
||||
@@ -821,6 +850,7 @@ export function getProfile(database, characterId, accountId) {
|
||||
inventory,
|
||||
gearStats,
|
||||
setBonuses,
|
||||
gearUpgradePaths,
|
||||
craftingRecipes: craftingRecipeRows.map((recipe) => {
|
||||
const components = craftingComponentRows
|
||||
.filter((component) => component.recipeId === recipe.id)
|
||||
@@ -829,6 +859,8 @@ export function getProfile(database, characterId, accountId) {
|
||||
quantity,
|
||||
owned,
|
||||
}))
|
||||
const hasRequiredComponents = components.length > 0
|
||||
&& components.every((component) => component.quantity > 0)
|
||||
const { itemId, slug, name, slot, rarity, itemLevel, healingPower, maxResourceBonus, glyph, description, setId, setSlug, setName } = recipe
|
||||
return {
|
||||
id: recipe.id,
|
||||
@@ -851,11 +883,13 @@ export function getProfile(database, characterId, accountId) {
|
||||
setName,
|
||||
},
|
||||
components,
|
||||
canCraft: components.every((component) => component.owned >= component.quantity),
|
||||
canCraft: hasRequiredComponents
|
||||
&& components.every((component) => component.owned >= component.quantity),
|
||||
}
|
||||
}),
|
||||
dungeons: dungeons.map((dungeon) => ({
|
||||
...dungeon,
|
||||
completionCount: dungeonCompletionCounts.get(dungeon.id) ?? 0,
|
||||
difficulties: dungeonDifficulties.filter(
|
||||
(difficulty) => difficulty.dungeonId === dungeon.id,
|
||||
),
|
||||
@@ -1692,11 +1726,17 @@ function craftItem(database, characterId, recipeId) {
|
||||
crafting_recipes.item_id AS itemId,
|
||||
crafting_recipes.difficulty_id AS difficultyId,
|
||||
crafting_recipes.source_dungeon_id AS sourceDungeonId,
|
||||
crafting_recipes.source_encounter_id AS sourceEncounterId
|
||||
crafting_recipes.source_encounter_id AS sourceEncounterId,
|
||||
items.slot,
|
||||
items.item_level AS itemLevel
|
||||
FROM crafting_recipes
|
||||
JOIN items ON items.id = crafting_recipes.item_id
|
||||
WHERE crafting_recipes.id = ?
|
||||
`).get(recipeId)
|
||||
if (!recipe) throw new Error('That crafting recipe does not exist.')
|
||||
if (!directCraftItemLevels.has(recipe.itemLevel)) {
|
||||
throw new Error('Upgrade the previous item tier instead.')
|
||||
}
|
||||
|
||||
const components = database.prepare(`
|
||||
SELECT
|
||||
@@ -1710,6 +1750,9 @@ function craftItem(database, characterId, recipeId) {
|
||||
WHERE crafting_recipe_components.recipe_id = ?
|
||||
`).all(characterId, recipeId)
|
||||
if (components.length === 0) throw new Error('That recipe has no component requirements.')
|
||||
if (components.some((component) => component.quantity <= 0)) {
|
||||
throw new Error('Recipe components must require at least one item.')
|
||||
}
|
||||
const missing = components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
const item = itemById(database, missing.itemId)
|
||||
@@ -1763,22 +1806,39 @@ function upgradeItem(database, characterId, itemId) {
|
||||
if (item.slot === componentSlot) throw new Error('Components cannot be upgraded.')
|
||||
|
||||
const currentRecipe = database.prepare(`
|
||||
SELECT source_encounter_id AS sourceEncounterId
|
||||
SELECT id
|
||||
FROM crafting_recipes
|
||||
WHERE item_id = ?
|
||||
`).get(itemId)
|
||||
if (!currentRecipe) throw new Error('No upgrade is available for this item.')
|
||||
|
||||
const targetRecipe = database.prepare(`
|
||||
const pathRecipe = database.prepare(`
|
||||
SELECT
|
||||
crafting_recipes.id,
|
||||
crafting_recipes.item_id AS itemId
|
||||
FROM gear_upgrade_paths
|
||||
JOIN crafting_recipes ON crafting_recipes.item_id = gear_upgrade_paths.to_item_id
|
||||
WHERE gear_upgrade_paths.from_item_id = ?
|
||||
`).get(itemId)
|
||||
|
||||
const targetRecipe = pathRecipe ?? database.prepare(`
|
||||
WITH next_tier AS (
|
||||
SELECT MIN(items.item_level) AS item_level
|
||||
FROM crafting_recipes
|
||||
JOIN items ON items.id = crafting_recipes.item_id
|
||||
WHERE items.slot = ?
|
||||
AND items.item_level > ?
|
||||
)
|
||||
SELECT
|
||||
crafting_recipes.id,
|
||||
crafting_recipes.item_id AS itemId
|
||||
FROM crafting_recipes
|
||||
JOIN items ON items.id = crafting_recipes.item_id
|
||||
WHERE crafting_recipes.source_encounter_id = ?
|
||||
AND items.slot = ?
|
||||
AND items.item_level = ?
|
||||
`).get(currentRecipe.sourceEncounterId, item.slot, item.itemLevel + 5)
|
||||
JOIN next_tier ON next_tier.item_level = items.item_level
|
||||
WHERE items.slot = ?
|
||||
ORDER BY crafting_recipes.id
|
||||
LIMIT 1
|
||||
`).get(item.slot, item.itemLevel, item.slot)
|
||||
if (!targetRecipe) throw new Error('No upgrade is available for this item.')
|
||||
|
||||
const components = database.prepare(`
|
||||
@@ -1792,6 +1852,10 @@ function upgradeItem(database, characterId, itemId) {
|
||||
AND character_inventory.character_id = ?
|
||||
WHERE crafting_recipe_components.recipe_id = ?
|
||||
`).all(characterId, targetRecipe.id)
|
||||
if (components.length === 0) throw new Error('That upgrade has no component requirements.')
|
||||
if (components.some((component) => component.quantity <= 0)) {
|
||||
throw new Error('Upgrade components must require at least one item.')
|
||||
}
|
||||
const missing = components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
const componentItem = itemById(database, missing.itemId)
|
||||
@@ -1841,9 +1905,20 @@ function upgradeItem(database, characterId, itemId) {
|
||||
return getProfile(database, characterId)
|
||||
}
|
||||
|
||||
function talentEffectCapacity(level) {
|
||||
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
||||
}
|
||||
|
||||
function talentEffectSource(effectType) {
|
||||
if (effectType.startsWith('mend_')) return 'Mend'
|
||||
if (effectType.startsWith('radiance_')) return 'Radiance'
|
||||
if (effectType.startsWith('shield_') || effectType.startsWith('shielded_')) return 'Shield'
|
||||
return effectType
|
||||
}
|
||||
|
||||
function allocateTalent(database, characterId, talentId) {
|
||||
const character = database.prepare(`
|
||||
SELECT class_id AS classId, talent_points AS talentPoints
|
||||
SELECT class_id AS classId, level, talent_points AS talentPoints
|
||||
FROM characters
|
||||
WHERE id = ?
|
||||
`).get(characterId)
|
||||
@@ -1855,7 +1930,8 @@ function allocateTalent(database, characterId, talentId) {
|
||||
max_rank AS maxRank,
|
||||
tier,
|
||||
prerequisite_talent_id AS prerequisiteTalentId,
|
||||
prerequisite_rank AS prerequisiteRank
|
||||
prerequisite_rank AS prerequisiteRank,
|
||||
effect_type AS effectType
|
||||
FROM talents
|
||||
WHERE id = ?
|
||||
`).get(talentId)
|
||||
@@ -1863,6 +1939,60 @@ function allocateTalent(database, characterId, talentId) {
|
||||
if (!talent || talent.classId !== character.classId) {
|
||||
throw new Error('That talent does not belong to the active class.')
|
||||
}
|
||||
|
||||
if (character.classId === 1) {
|
||||
const currentRank = database.prepare(`
|
||||
SELECT rank
|
||||
FROM character_talents
|
||||
WHERE character_id = ? AND talent_id = ?
|
||||
`).get(characterId, talentId)?.rank ?? 0
|
||||
database.exec('BEGIN')
|
||||
try {
|
||||
if (currentRank > 0) {
|
||||
database.prepare(`
|
||||
DELETE FROM character_talents
|
||||
WHERE character_id = ? AND talent_id = ?
|
||||
`).run(characterId, talentId)
|
||||
} else {
|
||||
const capacity = talentEffectCapacity(character.level)
|
||||
if (capacity <= 0) throw new Error('Spell effects unlock at level 5.')
|
||||
const activeTalents = database.prepare(`
|
||||
SELECT
|
||||
talents.id,
|
||||
talents.name,
|
||||
talents.effect_type AS effectType
|
||||
FROM character_talents
|
||||
JOIN talents ON talents.id = character_talents.talent_id
|
||||
WHERE character_talents.character_id = ?
|
||||
AND talents.class_id = ?
|
||||
AND character_talents.rank > 0
|
||||
`).all(characterId, character.classId)
|
||||
const source = talentEffectSource(talent.effectType)
|
||||
const sourceConflict = activeTalents.find(
|
||||
(candidate) => candidate.id !== talentId && talentEffectSource(candidate.effectType) === source,
|
||||
)
|
||||
if (sourceConflict) {
|
||||
throw new Error(`Only one ${source} spell effect can be active.`)
|
||||
}
|
||||
const activeCount = activeTalents.length
|
||||
if (activeCount >= capacity) {
|
||||
throw new Error(`Level ${character.level} allows ${capacity} active spell effect${capacity === 1 ? '' : 's'}.`)
|
||||
}
|
||||
database.prepare(`
|
||||
INSERT INTO character_talents (character_id, talent_id, rank)
|
||||
VALUES (?, ?, 1)
|
||||
ON CONFLICT(character_id, talent_id)
|
||||
DO UPDATE SET rank = 1
|
||||
`).run(characterId, talentId)
|
||||
}
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return getProfile(database, characterId)
|
||||
}
|
||||
|
||||
if (character.talentPoints <= 0) {
|
||||
throw new Error('No talent points are available.')
|
||||
}
|
||||
@@ -1941,11 +2071,13 @@ function resetTalents(database, characterId) {
|
||||
WHERE character_id = ?
|
||||
AND talent_id IN (SELECT id FROM talents WHERE class_id = ?)
|
||||
`).run(characterId, character.classId)
|
||||
database.prepare(`
|
||||
UPDATE characters
|
||||
SET talent_points = MIN(level, talent_points + ?)
|
||||
WHERE id = ?
|
||||
`).run(refunded, characterId)
|
||||
if (character.classId !== 1) {
|
||||
database.prepare(`
|
||||
UPDATE characters
|
||||
SET talent_points = MIN(level, talent_points + ?)
|
||||
WHERE id = ?
|
||||
`).run(refunded, characterId)
|
||||
}
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
@@ -2016,12 +2148,21 @@ function completeDungeon(database, characterId, accountId, dungeonId, difficulty
|
||||
const completedPart = Math.min(Math.max(Number(runMetrics?.completedPart) || 1, 1), 3)
|
||||
const startPart = Math.min(Math.max(Number(runMetrics?.startPart) || 1, 1), 3)
|
||||
const completedParts = completedPart - startPart + 1
|
||||
const rewardMultiplier = runMetrics?.hardMode ? 2 : 1
|
||||
const rawPartDurations = runMetrics?.partDurationSeconds
|
||||
const partDurationSeconds = Array.isArray(rawPartDurations) && rawPartDurations.length === 3
|
||||
? rawPartDurations.map(Number)
|
||||
: null
|
||||
const experienceReward = Math.round(
|
||||
dungeon.experienceReward * dungeon.experienceMultiplier * completedPart,
|
||||
const baseExperienceReward = Math.round(
|
||||
dungeon.experienceReward * dungeon.experienceMultiplier * completedPart * rewardMultiplier,
|
||||
)
|
||||
const experienceReward = catchUpExperienceReward(
|
||||
database,
|
||||
accountId,
|
||||
characterId,
|
||||
baseExperienceReward,
|
||||
character.experience,
|
||||
character.level,
|
||||
)
|
||||
const newExperience = Math.min(character.experience + experienceReward, maxExperience)
|
||||
const newLevel = database.prepare(`
|
||||
@@ -2119,17 +2260,18 @@ function completeDungeon(database, characterId, accountId, dungeonId, difficulty
|
||||
`).all(dungeonId, dungeon.completionItemLevel ?? dungeon.droppedItemLevel + 3)
|
||||
if (bonusItems.length > 0) {
|
||||
bonusItem = bonusItems[0]
|
||||
const rewardQuantity = rewardMultiplier
|
||||
const previousQuantity = database.prepare(`
|
||||
SELECT quantity FROM character_inventory
|
||||
WHERE character_id = ? AND item_id = ?
|
||||
`).get(characterId, bonusItem.id)?.quantity ?? 0
|
||||
database.prepare(`
|
||||
INSERT INTO character_inventory (character_id, item_id, quantity, equipped)
|
||||
VALUES (?, ?, 1, 0)
|
||||
VALUES (?, ?, ?, 0)
|
||||
ON CONFLICT(character_id, item_id)
|
||||
DO UPDATE SET quantity = quantity + 1
|
||||
`).run(characterId, bonusItem.id)
|
||||
bonusItem = { ...bonusItem, quantity: 1, duplicate: previousQuantity > 0, quantityAfter: previousQuantity + 1 }
|
||||
DO UPDATE SET quantity = quantity + ?
|
||||
`).run(characterId, bonusItem.id, rewardQuantity, rewardQuantity)
|
||||
bonusItem = { ...bonusItem, quantity: rewardQuantity, duplicate: previousQuantity > 0, quantityAfter: previousQuantity + rewardQuantity }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2226,6 +2368,12 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
|
||||
let newExperience = character.experience
|
||||
let newLevel = character.level
|
||||
if (experienceMode === 'pvp-boss-quarter-level') {
|
||||
const catchUpTargetLevel = database.prepare(`
|
||||
SELECT COALESCE(MAX(level), 0) AS level
|
||||
FROM characters
|
||||
WHERE account_id = ?
|
||||
AND id != ?
|
||||
`).get(accountId, characterId).level
|
||||
for (let bossIndex = 0; bossIndex < bossesCleared && newExperience < maxExperience; bossIndex += 1) {
|
||||
const currentLevelFloor = database.prepare(`
|
||||
SELECT experience_required AS experienceRequired
|
||||
@@ -2240,7 +2388,8 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
|
||||
WHERE level = ?
|
||||
`).get(newLevel + 1).experienceRequired
|
||||
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
|
||||
newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * 0.25))
|
||||
const rewardRate = catchUpTargetLevel > newLevel ? 0.5 : 0.25
|
||||
newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * rewardRate))
|
||||
newLevel = database.prepare(`
|
||||
SELECT MAX(level) AS level
|
||||
FROM level_progression
|
||||
@@ -2248,9 +2397,17 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
|
||||
`).get(newExperience).level
|
||||
}
|
||||
} else {
|
||||
const experienceReward = Math.round(
|
||||
const baseExperienceReward = Math.round(
|
||||
dungeon.experienceReward * dungeon.experienceMultiplier * (encountersCleared / 3),
|
||||
)
|
||||
const experienceReward = catchUpExperienceReward(
|
||||
database,
|
||||
accountId,
|
||||
characterId,
|
||||
baseExperienceReward,
|
||||
character.experience,
|
||||
character.level,
|
||||
)
|
||||
newExperience = Math.min(character.experience + experienceReward, maxExperience)
|
||||
newLevel = database.prepare(`
|
||||
SELECT MAX(level) AS level
|
||||
@@ -2540,6 +2697,11 @@ export async function handleApiRequest(request, response, next) {
|
||||
return
|
||||
}
|
||||
|
||||
if (request.url === '/api/catalog' && request.method === 'GET') {
|
||||
sendJson(response, 200, catalogPayload(getProfile(database, 1)))
|
||||
return
|
||||
}
|
||||
|
||||
const session = requireSession(database, request)
|
||||
|
||||
if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
|
||||
|
||||
@@ -55,6 +55,7 @@ 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
|
||||
@@ -81,13 +82,14 @@ 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)
|
||||
@@ -131,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])
|
||||
@@ -148,6 +157,9 @@ function App() {
|
||||
setScreen('menu')
|
||||
setError('')
|
||||
setServerMessage('')
|
||||
window.requestAnimationFrame(() => {
|
||||
focusFirstControl()
|
||||
})
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
@@ -220,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')
|
||||
}}
|
||||
@@ -272,21 +285,54 @@ 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 cloudSync = getCloudSyncStatus()
|
||||
const canShowCloudSync = account.id !== -1 && cloudSync.available
|
||||
const lootPreviewEncounters = [...activity.encounters]
|
||||
@@ -296,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"
|
||||
@@ -393,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' && (
|
||||
@@ -554,108 +606,196 @@ function App() {
|
||||
)}
|
||||
|
||||
{(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>
|
||||
@@ -722,9 +862,9 @@ function App() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{SHOW_LEADERBOARDS && (
|
||||
<div className="leaderboard-section">
|
||||
</div>
|
||||
{SHOW_LEADERBOARDS && (
|
||||
<div className="leaderboard-section">
|
||||
<div className="equipment-heading toggle-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Efficiency Rankings</p>
|
||||
@@ -749,10 +889,10 @@ function App() {
|
||||
</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}
|
||||
@@ -801,8 +941,10 @@ function App() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type EquipmentSlot,
|
||||
type Item,
|
||||
} from '../profile'
|
||||
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
|
||||
|
||||
const SLOT_LABELS: Record<EquipmentSlot, string> = {
|
||||
weapon: 'Weapon',
|
||||
@@ -24,16 +25,49 @@ const SLOT_LABELS: Record<EquipmentSlot, string> = {
|
||||
}
|
||||
|
||||
const EQUIPMENT_LIST_PAGE_SIZE = 3
|
||||
const CRAFTING_LIST_PAGE_SIZE = 6
|
||||
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,
|
||||
@@ -49,27 +83,29 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = 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
|
||||
? profile.craftingRecipes.find((recipe) =>
|
||||
recipe.sourceEncounterId === selectedItemRecipe.sourceEncounterId
|
||||
&& recipe.item.slot === selectedItem.slot
|
||||
&& recipe.item.itemLevel === selectedItem.itemLevel + 5,
|
||||
)
|
||||
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, selectedItem)
|
||||
: undefined
|
||||
const equippedBySlot = useMemo(
|
||||
() => new Map(
|
||||
@@ -104,12 +140,14 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
||||
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)
|
||||
@@ -117,6 +155,19 @@ 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),
|
||||
@@ -138,12 +189,26 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
||||
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
|
||||
}
|
||||
@@ -218,6 +283,143 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = 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 && (
|
||||
@@ -244,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' ? (
|
||||
<>
|
||||
@@ -268,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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -284,41 +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 || 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>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
@@ -326,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" />
|
||||
@@ -421,61 +591,109 @@ 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')
|
||||
setRecipePage(0)
|
||||
}}
|
||||
>
|
||||
<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))
|
||||
setRecipePage(0)
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
{recipePageItems.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>
|
||||
<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}`}
|
||||
@@ -485,38 +703,52 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
|
||||
previousDisabled={recipePage <= 0}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
@@ -552,11 +784,11 @@ 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>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { INITIAL_PARTY, RAID_PARTY, type CombatLogEntry, type PartyMember, type Spell } from '../game'
|
||||
import {
|
||||
INITIAL_PARTY,
|
||||
RAID_PARTY,
|
||||
DEFAULT_GROUP_HEAL_TARGETS,
|
||||
groupHealTargets,
|
||||
partyDamageOutput,
|
||||
tankPressureTargets,
|
||||
type CombatLogEntry,
|
||||
type PartyMember,
|
||||
type Spell,
|
||||
} from '../game'
|
||||
import { completeRoguelike, type DungeonReward } from '../profile'
|
||||
import type { Ability, CharacterProfile, DungeonEncounter } from '../profile'
|
||||
import type { GameMode } from '../gameRepository'
|
||||
@@ -34,7 +44,7 @@ type PvpEncounter = DungeonEncounter & {
|
||||
sourceEncounterId?: number
|
||||
}
|
||||
|
||||
type SlotKey = '1' | '2' | '3' | '4' | '5'
|
||||
type SlotKey = '1' | '2' | '3' | '4' | '5' | '6'
|
||||
type AbilityLabelMode = 'ability' | 'slot'
|
||||
|
||||
type SelfBuffId =
|
||||
@@ -78,6 +88,17 @@ type FloatingCombatText = {
|
||||
value: number
|
||||
}
|
||||
|
||||
type PvpRunSummary = {
|
||||
bossesKilled: number
|
||||
experienceGained: number
|
||||
previousLevel: number | null
|
||||
newLevel: number | null
|
||||
levelsGained: number
|
||||
talentPointsGained: number
|
||||
unlockedAbilities: DungeonReward['unlockedAbilities']
|
||||
loot: Array<NonNullable<DungeonReward['bonusItem']>>
|
||||
}
|
||||
|
||||
const BOSS_MECHANICS: BossMechanic[] = [
|
||||
'party-pulse',
|
||||
'searing-mark',
|
||||
@@ -119,6 +140,19 @@ function formatEffectTime(ticks: number) {
|
||||
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`
|
||||
}
|
||||
|
||||
function createEmptyPvpRunSummary(): PvpRunSummary {
|
||||
return {
|
||||
bossesKilled: 0,
|
||||
experienceGained: 0,
|
||||
previousLevel: null,
|
||||
newLevel: null,
|
||||
levelsGained: 0,
|
||||
talentPointsGained: 0,
|
||||
unlockedAbilities: [],
|
||||
loot: [],
|
||||
}
|
||||
}
|
||||
|
||||
function buffStacks<T extends string>(items: T[], id: T) {
|
||||
return items.filter((item) => item === id).length
|
||||
}
|
||||
@@ -130,7 +164,7 @@ function slotLabel(slot: SlotKey, spells: Spell[], labelMode: AbilityLabelMode)
|
||||
}
|
||||
|
||||
function buildSelfBuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<SelfBuffId>> {
|
||||
const slotChoices = (['1', '2', '3', '4', '5'] as SlotKey[]).flatMap((slot) => {
|
||||
const slotChoices = (['1', '2', '3', '4', '5', '6'] as SlotKey[]).flatMap((slot) => {
|
||||
const label = slotLabel(slot, spells, labelMode)
|
||||
return [
|
||||
{
|
||||
@@ -159,7 +193,7 @@ function buildSelfBuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Arr
|
||||
}
|
||||
|
||||
function buildOpponentDebuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<OpponentDebuffId>> {
|
||||
const slotChoices = (['1', '2', '3', '4', '5'] as SlotKey[]).flatMap((slot) => {
|
||||
const slotChoices = (['1', '2', '3', '4', '5', '6'] as SlotKey[]).flatMap((slot) => {
|
||||
const label = slotLabel(slot, spells, labelMode)
|
||||
return [
|
||||
{
|
||||
@@ -217,13 +251,13 @@ function outgoingHealMultiplier(debuffs: OpponentDebuffId[]) {
|
||||
return 0.85 ** buffStacks(debuffs, 'opp-healing-reduced')
|
||||
}
|
||||
|
||||
function healAmount(member: PartyMember, amount: number, debuffs: OpponentDebuffId[]) {
|
||||
function healAmount(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) {
|
||||
const healingReduction = member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1
|
||||
return Math.round(amount * healingReduction * outgoingHealMultiplier(debuffs))
|
||||
return Math.round(amount * healingReduction * outgoingHealMultiplier(debuffs) * multiplier)
|
||||
}
|
||||
|
||||
function healMember(member: PartyMember, amount: number, debuffs: OpponentDebuffId[]) {
|
||||
return clamp(member.health + healAmount(member, amount, debuffs), 0, effectiveMaxHealth(member))
|
||||
function healMember(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) {
|
||||
return clamp(member.health + healAmount(member, amount, debuffs, multiplier), 0, effectiveMaxHealth(member))
|
||||
}
|
||||
|
||||
function cooldownMultiplier(spell: Spell, buffs: SelfBuffId[], debuffs: OpponentDebuffId[]) {
|
||||
@@ -300,7 +334,7 @@ function scoreSelfBuff(buff: Choice<SelfBuffId>, spells: Spell[]) {
|
||||
if (buff.id === 'fifth-cast-free') return 8
|
||||
if (buff.id === 'group-heal-boost') return 8
|
||||
if (buff.id === 'shield-boost') return 6
|
||||
const slot = buff.id.match(/slot([1-5])/i)?.[1] as SlotKey | undefined
|
||||
const slot = buff.id.match(/slot([1-6])/i)?.[1] as SlotKey | undefined
|
||||
const spell = spells.find((candidate) => candidate.key === slot)
|
||||
if (!spell) return 5
|
||||
if (buff.id.endsWith('extra-target')) {
|
||||
@@ -374,7 +408,7 @@ export function PvPRoguelikeScreen({
|
||||
const gameClass = profile.classes.find((candidate) => candidate.id === profile.character.classId)!
|
||||
const starterSpells = useMemo(() => gameClass.spells
|
||||
.filter((spell) => spell.unlockLevel === 1)
|
||||
.slice(0, 5)
|
||||
.slice(0, 6)
|
||||
.map((spell, index) => toCombatSpell(spell, String(index + 1))), [gameClass.spells])
|
||||
const [abilityLabelMode] = useState<AbilityLabelMode>('ability')
|
||||
const selfBuffChoicesCatalog = useMemo(
|
||||
@@ -411,11 +445,14 @@ export function PvPRoguelikeScreen({
|
||||
const [playerSide, setPlayerSide] = useState<SideState>(() => starterSide(partyTemplate, maxResource))
|
||||
const [cpuSide, setCpuSide] = useState<SideState>(() => starterSide(cpuPartyTemplate, maxResource))
|
||||
const [selectedId, setSelectedId] = useState(partyTemplate[0].id)
|
||||
const selectedIdRef = useRef(partyTemplate[0].id)
|
||||
const [speedMultiplier, setSpeedMultiplier] = useState<1 | 2>(1)
|
||||
const [elapsedTicks, setElapsedTicks] = useState(0)
|
||||
const [cpuDifficulty, setCpuDifficulty] = useState<CpuDifficulty | null>(null)
|
||||
const [queueMessage, setQueueMessage] = useState('')
|
||||
const [log, setLog] = useState<CombatLogEntry[]>([{ id: 1, text: 'Queueing opponent...', tone: 'system' }])
|
||||
const [reward, setReward] = useState<DungeonReward | null>(null)
|
||||
const [runSummary, setRunSummary] = useState<PvpRunSummary>(() => createEmptyPvpRunSummary())
|
||||
const [rewardError, setRewardError] = useState('')
|
||||
const [showEndLog, setShowEndLog] = useState(false)
|
||||
const [floatingTexts, setFloatingTexts] = useState<FloatingCombatText[]>([])
|
||||
@@ -433,6 +470,8 @@ export function PvPRoguelikeScreen({
|
||||
const bossRewardClaimedRef = useRef(new Set<number>())
|
||||
const cpuDefeatedRef = useRef(false)
|
||||
const playerClearedEncounterRef = useRef(-1)
|
||||
const queuedMatchRef = useRef(false)
|
||||
const encounterPoolRef = useRef(encounterPool)
|
||||
const playerRef = useRef(playerSide)
|
||||
const cpuRef = useRef(cpuSide)
|
||||
const encounter = encounters[encounterIndex]
|
||||
@@ -445,6 +484,14 @@ export function PvPRoguelikeScreen({
|
||||
? Math.max(encountersCleared, encounterIndex + 1)
|
||||
: encountersCleared
|
||||
const cpuBehavior = cpuDifficulty ? CPU_BEHAVIOR[cpuDifficulty] : CPU_BEHAVIOR[1]
|
||||
const activeSpellEffects = useMemo(
|
||||
() => new Set(
|
||||
gameClass.talents
|
||||
.filter((talent) => talent.rank > 0)
|
||||
.map((talent) => talent.effectType),
|
||||
),
|
||||
[gameClass.talents],
|
||||
)
|
||||
const playerDone = playerSide.enemyHealth <= 0
|
||||
const cpuDone = cpuSide.enemyHealth <= 0
|
||||
const playerAlive = playerSide.party.some((member) => member.health > 0)
|
||||
@@ -459,6 +506,12 @@ export function PvPRoguelikeScreen({
|
||||
const {
|
||||
enabled: dualScreenEnabled,
|
||||
} = useDualScreen()
|
||||
|
||||
const setSelectedTargetId = useCallback((id: string) => {
|
||||
selectedIdRef.current = id
|
||||
setSelectedId(id)
|
||||
}, [])
|
||||
|
||||
const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
|
||||
setLog((current) => [createLogEntry(nextLogId, text, tone), ...current].slice(0, 60))
|
||||
}, [])
|
||||
@@ -473,11 +526,16 @@ export function PvPRoguelikeScreen({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (queuedMatchRef.current) return
|
||||
const loadedCheckpoint = loadPvpRoguelikeCheckpoint(profile.character.id, contentType)
|
||||
setCheckpointStage(loadedCheckpoint)
|
||||
setStartStage(loadedCheckpoint)
|
||||
}, [contentType, profile.character.id])
|
||||
|
||||
useEffect(() => {
|
||||
encounterPoolRef.current = encounterPool
|
||||
}, [encounterPool])
|
||||
|
||||
const awardBossReward = useCallback((encounterIndexValue: number) => {
|
||||
if (bossRewardClaimedRef.current.has(encounterIndexValue)) return
|
||||
bossRewardClaimedRef.current.add(encounterIndexValue)
|
||||
@@ -497,6 +555,20 @@ export function PvPRoguelikeScreen({
|
||||
)
|
||||
.then((result) => {
|
||||
setReward(result)
|
||||
setRunSummary((current) => {
|
||||
const unlockedById = new Map(current.unlockedAbilities.map((ability) => [ability.id, ability]))
|
||||
result.unlockedAbilities.forEach((ability) => unlockedById.set(ability.id, ability))
|
||||
return {
|
||||
bossesKilled: current.bossesKilled + 1,
|
||||
experienceGained: current.experienceGained + result.experienceGained,
|
||||
previousLevel: current.previousLevel ?? result.previousLevel,
|
||||
newLevel: result.newLevel,
|
||||
levelsGained: current.levelsGained + result.levelsGained,
|
||||
talentPointsGained: current.talentPointsGained + result.talentPointsGained,
|
||||
unlockedAbilities: Array.from(unlockedById.values()),
|
||||
loot: result.bonusItem ? [...current.loot, result.bonusItem] : current.loot,
|
||||
}
|
||||
})
|
||||
onProfileUpdated(result.profile)
|
||||
if (result.bonusItem) {
|
||||
addLog(
|
||||
@@ -532,8 +604,9 @@ export function PvPRoguelikeScreen({
|
||||
: null)
|
||||
}, [opponentDebuffChoicesCatalog, selfBuffChoicesCatalog])
|
||||
|
||||
useEffect(() => {
|
||||
const firstSegment = buildEncounterSegment(encounterPool, startStage, contentType)
|
||||
const startMatch = useCallback((nextStartStage?: number) => {
|
||||
const matchStartStage = nextStartStage ?? loadPvpRoguelikeCheckpoint(profile.character.id, contentType)
|
||||
const firstSegment = buildEncounterSegment(encounterPoolRef.current, matchStartStage, contentType)
|
||||
const firstEncounter = firstSegment[0]
|
||||
const basePlayer = starterSide(partyTemplate, maxResource)
|
||||
const baseCpu = starterSide(cpuPartyTemplate, maxResource)
|
||||
@@ -543,15 +616,18 @@ export function PvPRoguelikeScreen({
|
||||
cpuRef.current = baseCpu
|
||||
nextLogId.current = 2
|
||||
playerClearedEncounterRef.current = -1
|
||||
queuedMatchRef.current = true
|
||||
bossRewardClaimedRef.current = new Set()
|
||||
setEncounters(firstSegment)
|
||||
setEncounterIndex(0)
|
||||
setStage(startStage)
|
||||
setCheckpointStage(matchStartStage)
|
||||
setStartStage(matchStartStage)
|
||||
setStage(matchStartStage)
|
||||
setElapsedTicks(0)
|
||||
setStatus('queueing')
|
||||
setPlayerSide(basePlayer)
|
||||
setCpuSide(baseCpu)
|
||||
setSelectedId(partyTemplate[0].id)
|
||||
setSelectedTargetId(partyTemplate[0].id)
|
||||
setPlayerBuffChoices([])
|
||||
setPlayerDebuffChoices([])
|
||||
setSelectedBuff(null)
|
||||
@@ -560,6 +636,7 @@ export function PvPRoguelikeScreen({
|
||||
setPaused(false)
|
||||
setTargetGroup(0)
|
||||
setReward(null)
|
||||
setRunSummary(createEmptyPvpRunSummary())
|
||||
setRewardError('')
|
||||
setShowEndLog(false)
|
||||
setFloatingTexts([])
|
||||
@@ -569,26 +646,28 @@ export function PvPRoguelikeScreen({
|
||||
cpuDefeatedRef.current = false
|
||||
if (gameMode === 'offline') {
|
||||
const randomCpu = randomCpuDifficulty()
|
||||
setQueueMessage(`Offline mode. CPU ${randomCpu} enters at stage ${startStage}.`)
|
||||
setQueueMessage(`Offline mode. CPU ${randomCpu} enters at stage ${matchStartStage}.`)
|
||||
setCpuDifficulty(randomCpu)
|
||||
setLog([{ id: 1, text: `Offline mode. CPU ${randomCpu} enters at stage ${startStage}.`, tone: 'system' }])
|
||||
setLog([{ id: 1, text: `Offline mode. CPU ${randomCpu} enters at stage ${matchStartStage}.`, tone: 'system' }])
|
||||
const timer = window.setTimeout(() => {
|
||||
setStatus('playing')
|
||||
addLog(`Stage ${startStage} begins against CPU ${randomCpu}.`, 'system')
|
||||
addLog(`Stage ${matchStartStage} begins against CPU ${randomCpu}.`, 'system')
|
||||
}, 500)
|
||||
return () => window.clearTimeout(timer)
|
||||
}
|
||||
setQueueMessage(`Searching queue. Stage ${startStage} start ready.`)
|
||||
setLog([{ id: 1, text: `Searching queue. Stage ${startStage} start ready.`, tone: 'system' }])
|
||||
setQueueMessage(`Searching queue. Stage ${matchStartStage} start ready.`)
|
||||
setLog([{ id: 1, text: `Searching queue. Stage ${matchStartStage} start ready.`, tone: 'system' }])
|
||||
const timer = window.setTimeout(() => {
|
||||
const randomCpu = randomCpuDifficulty()
|
||||
setCpuDifficulty(randomCpu)
|
||||
setQueueMessage(`No queued player found. CPU ${randomCpu} steps in.`)
|
||||
setStatus('playing')
|
||||
addLog(`No queued player found. CPU ${randomCpu} steps in at stage ${startStage}.`, 'system')
|
||||
addLog(`No queued player found. CPU ${randomCpu} steps in at stage ${matchStartStage}.`, 'system')
|
||||
}, 1400)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [addLog, contentType, cpuPartyTemplate, encounterPool, gameMode, maxResource, partyTemplate, startStage])
|
||||
}, [addLog, contentType, cpuPartyTemplate, gameMode, maxResource, partyTemplate, profile.character.id, setSelectedTargetId])
|
||||
|
||||
useEffect(() => startMatch(), [startMatch])
|
||||
|
||||
const applySpell = useCallback((
|
||||
current: SideState,
|
||||
@@ -607,10 +686,21 @@ export function PvPRoguelikeScreen({
|
||||
const extraTarget = (blockedIds: string[]) => livingTargets
|
||||
.filter((member) => !blockedIds.includes(member.id))
|
||||
.sort((left, right) => (left.health / left.maxHealth) - (right.health / right.maxHealth))[0]
|
||||
const hasSpellEffect = (effectType: string) => sideName === 'player' && activeSpellEffects.has(effectType)
|
||||
const renewEffect = starterSpells.find((candidate) => candidate.kind === 'hot')
|
||||
const shieldEffect = starterSpells.find((candidate) => candidate.kind === 'shield')
|
||||
const radianceEffect = starterSpells.find((candidate) => candidate.kind === 'group')
|
||||
const healingMultiplier = (member: PartyMember) =>
|
||||
hasSpellEffect('shielded_healing_bonus') && member.shield > 0 ? 1.2 : 1
|
||||
const directTargets = new Set([targetId])
|
||||
const hotTargets = new Set(spell.kind === 'hot' ? [targetId] : [])
|
||||
const shieldTargets = new Set(spell.kind === 'shield' ? [targetId] : [])
|
||||
const extraTargets = buffStacks(buffs, `slot${spell.key as SlotKey}-extra-target` as SelfBuffId)
|
||||
const groupTargets = new Set(
|
||||
spell.kind === 'group'
|
||||
? groupHealTargets(current.party, DEFAULT_GROUP_HEAL_TARGETS + extraTargets).map((member) => member.id)
|
||||
: [],
|
||||
)
|
||||
for (let index = 0; index < extraTargets; index += 1) {
|
||||
if (spell.kind === 'group') break
|
||||
if (spell.kind === 'hot') {
|
||||
@@ -626,21 +716,45 @@ export function PvPRoguelikeScreen({
|
||||
const extra = extraTarget([...directTargets])
|
||||
if (extra) directTargets.add(extra.id)
|
||||
}
|
||||
if (spell.kind === 'direct' && hasSpellEffect('mend_applies_renew') && renewEffect) {
|
||||
directTargets.forEach((id) => hotTargets.add(id))
|
||||
}
|
||||
if (spell.kind === 'direct' && hasSpellEffect('mend_applies_shield') && shieldEffect) {
|
||||
directTargets.forEach((id) => shieldTargets.add(id))
|
||||
}
|
||||
if (spell.kind === 'shield' && hasSpellEffect('shield_applies_renew') && renewEffect) {
|
||||
shieldTargets.forEach((id) => hotTargets.add(id))
|
||||
}
|
||||
const nextParty = current.party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
if (spell.kind === 'group') {
|
||||
if (!groupTargets.has(member.id)) return member
|
||||
const groupPower = Math.round(spell.power * (1.25 ** buffStacks(buffs, 'group-heal-boost')))
|
||||
const nextHealth = healMember(member, groupPower, debuffs)
|
||||
const nextHealth = healMember(member, groupPower, debuffs, healingMultiplier(member))
|
||||
addFloatingHeal(sideName, member.id, Math.max(0, nextHealth - member.health))
|
||||
return { ...member, health: nextHealth }
|
||||
const nextShield = hasSpellEffect('radiance_applies_shield')
|
||||
? Math.max(member.shield, Math.round((shieldEffect?.power ?? spell.power) * 0.3))
|
||||
: member.shield
|
||||
return {
|
||||
...member,
|
||||
health: nextHealth,
|
||||
shield: nextShield,
|
||||
hotTicks: hasSpellEffect('radiance_applies_renew') && renewEffect
|
||||
? Math.max(member.hotTicks, 3)
|
||||
: member.hotTicks,
|
||||
}
|
||||
}
|
||||
if (!directTargets.has(member.id) && !hotTargets.has(member.id) && !shieldTargets.has(member.id)) return member
|
||||
if (spell.kind === 'shield') {
|
||||
const shieldPower = Math.round(spell.power * (1.25 ** buffStacks(buffs, 'shield-boost')))
|
||||
return { ...member, shield: Math.max(member.shield, shieldPower) }
|
||||
return {
|
||||
...member,
|
||||
shield: Math.max(member.shield, shieldPower),
|
||||
hotTicks: hotTargets.has(member.id) ? 5 : member.hotTicks,
|
||||
}
|
||||
}
|
||||
if (spell.kind === 'cleanse') {
|
||||
const nextHealth = healMember(member, spell.power, debuffs)
|
||||
const nextHealth = healMember(member, spell.power, debuffs, healingMultiplier(member))
|
||||
addFloatingHeal(sideName, member.id, Math.max(0, nextHealth - member.health))
|
||||
return {
|
||||
...member,
|
||||
@@ -652,11 +766,17 @@ export function PvPRoguelikeScreen({
|
||||
healingReductionTicks: undefined,
|
||||
}
|
||||
}
|
||||
const nextHealth = directTargets.has(member.id) ? healMember(member, spell.power, debuffs) : member.health
|
||||
const nextHealth = directTargets.has(member.id)
|
||||
? healMember(member, spell.power, debuffs, healingMultiplier(member))
|
||||
: member.health
|
||||
if (nextHealth > member.health) addFloatingHeal(sideName, member.id, nextHealth - member.health)
|
||||
const nextShield = shieldTargets.has(member.id) && spell.kind === 'direct' && hasSpellEffect('mend_applies_shield')
|
||||
? Math.max(member.shield, Math.round((shieldEffect?.power ?? spell.power) * 0.5))
|
||||
: member.shield
|
||||
return {
|
||||
...member,
|
||||
health: nextHealth,
|
||||
shield: nextShield,
|
||||
hotTicks: hotTargets.has(member.id) ? 5 : member.hotTicks,
|
||||
}
|
||||
})
|
||||
@@ -670,46 +790,51 @@ export function PvPRoguelikeScreen({
|
||||
: current.castsTowardFree + 1
|
||||
: current.castsTowardFree
|
||||
const gainedFreeCast = freeCastStacks > 0 && !current.freeCastReady && current.castsTowardFree + 1 >= 5
|
||||
const nextCooldowns = {
|
||||
...current.cooldowns,
|
||||
}
|
||||
if (spell.kind === 'direct' && hasSpellEffect('mend_reduces_radiance_cooldown') && radianceEffect) {
|
||||
nextCooldowns[radianceEffect.id] = Math.max(0, (nextCooldowns[radianceEffect.id] ?? 0) - 2)
|
||||
}
|
||||
nextCooldowns[spell.id] = spell.cooldown * cooldownMultiplier(spell, buffs, debuffs)
|
||||
const nextState: SideState = {
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: current.resource - effectiveCost,
|
||||
cooldowns: {
|
||||
...current.cooldowns,
|
||||
[spell.id]: spell.cooldown * cooldownMultiplier(spell, buffs, debuffs),
|
||||
},
|
||||
cooldowns: nextCooldowns,
|
||||
castsTowardFree: nextCastsTowardFree,
|
||||
freeCastReady: gainedFreeCast || nextFreeCastReady,
|
||||
}
|
||||
setCurrent(nextState)
|
||||
return true
|
||||
}, [addFloatingHeal])
|
||||
}, [activeSpellEffects, addFloatingHeal, starterSpells])
|
||||
|
||||
const castPlayerSpell = useCallback((spell: Spell) => {
|
||||
if (status !== 'playing' || playerDone || !playerAlive) return
|
||||
const targetId = selectedIdRef.current
|
||||
const succeeded = applySpell(playerRef.current, (value) => {
|
||||
const next = typeof value === 'function' ? value(playerRef.current) : value
|
||||
playerRef.current = next
|
||||
setPlayerSide(next)
|
||||
}, 'player', playerRef.current.buffs, playerRef.current.debuffs, spell, selectedId)
|
||||
if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === selectedId)?.name ?? 'target'}.`, 'heal')
|
||||
}, [addLog, applySpell, playerAlive, playerDone, selectedId, status])
|
||||
}, 'player', playerRef.current.buffs, playerRef.current.debuffs, spell, targetId)
|
||||
if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal')
|
||||
}, [addLog, applySpell, playerAlive, playerDone, status])
|
||||
|
||||
const selectRelativeTarget = useCallback((direction: -1 | 1) => {
|
||||
const living = playerRef.current.party.filter((member) => member.health > 0)
|
||||
if (living.length === 0) return
|
||||
const currentIndex = living.findIndex((member) => member.id === selectedId)
|
||||
const currentIndex = living.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const nextIndex = currentIndex < 0
|
||||
? 0
|
||||
: (currentIndex + direction + living.length) % living.length
|
||||
setSelectedId(living[nextIndex].id)
|
||||
}, [selectedId])
|
||||
setSelectedTargetId(living[nextIndex].id)
|
||||
}, [setSelectedTargetId])
|
||||
|
||||
const selectDirectionalTarget = useCallback((action: InputAction) => {
|
||||
const currentIndex = playerRef.current.party.findIndex((member) => member.id === selectedId)
|
||||
const currentIndex = playerRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
|
||||
if (currentIndex < 0) {
|
||||
const firstLiving = playerRef.current.party.find((member) => member.health > 0)
|
||||
if (firstLiving) setSelectedId(firstLiving.id)
|
||||
if (firstLiving) setSelectedTargetId(firstLiving.id)
|
||||
return
|
||||
}
|
||||
const currentRow = Math.floor(currentIndex / partyColumns)
|
||||
@@ -736,14 +861,14 @@ export function PvPRoguelikeScreen({
|
||||
const bSecondary = horizontal ? 0 : Math.abs(b.column - currentColumn)
|
||||
return aPrimary - bPrimary || aSecondary - bSecondary
|
||||
})
|
||||
if (candidates[0]) setSelectedId(candidates[0].member.id)
|
||||
}, [partyColumns, selectedId])
|
||||
if (candidates[0]) setSelectedTargetId(candidates[0].member.id)
|
||||
}, [partyColumns, setSelectedTargetId])
|
||||
|
||||
const selectDirectTarget = useCallback((slot: number) => {
|
||||
const index = slot + (contentType === 'raid' ? targetGroup * 6 : 0)
|
||||
const member = playerRef.current.party[index]
|
||||
if (member?.health > 0) setSelectedId(member.id)
|
||||
}, [contentType, targetGroup])
|
||||
if (member?.health > 0) setSelectedTargetId(member.id)
|
||||
}, [contentType, setSelectedTargetId, targetGroup])
|
||||
|
||||
const cpuTakeTurn = useCallback(() => {
|
||||
if (!cpuDifficulty || status !== 'playing' || cpuDone || !cpuAlive) return
|
||||
@@ -790,10 +915,15 @@ export function PvPRoguelikeScreen({
|
||||
const appliesHealingReduction = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 9 === 0 && mechanics.includes('healing-reduction')
|
||||
const appliesPoison = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 12 === 0 && mechanics.includes('ramping-poison')
|
||||
const damageMultiplier = incomingDamageMultiplier(side.debuffs)
|
||||
const hasSpellEffect = (effectType: string) => sideName === 'player' && activeSpellEffects.has(effectType)
|
||||
const tankPressure = tankPressureTargets(side.party)
|
||||
const tankPressureIds = new Set(tankPressure.targets.map((member) => member.id))
|
||||
const nextParty = side.party.map((member) => {
|
||||
if (member.health <= 0) return member
|
||||
let damage = member.id === primaryTarget.id ? encounterValue.damage : 0
|
||||
if (member.role === 'Tank') damage += encounterValue.tankDamage
|
||||
if (tankPressureIds.has(member.id)) {
|
||||
damage += Math.round(encounterValue.tankDamage * tankPressure.multiplier)
|
||||
}
|
||||
if (bossPulse) damage += 10
|
||||
if (member.debuff) damage += 6
|
||||
const nextPoisonStacks = appliesPoison && member.id === primaryTarget.id
|
||||
@@ -801,8 +931,12 @@ export function PvPRoguelikeScreen({
|
||||
: member.poisonStacks ?? 0
|
||||
if (nextPoisonStacks > 0) damage += 3 + nextPoisonStacks * 3
|
||||
damage = Math.round(damage * damageMultiplier)
|
||||
if (member.shield > 0 && hasSpellEffect('shielded_damage_reduction')) {
|
||||
damage = Math.round(damage * 0.8)
|
||||
}
|
||||
const absorbed = Math.min(member.shield, damage)
|
||||
const healing = member.hotTicks > 0 ? healAmount(member, 6, side.debuffs) : 0
|
||||
const healingMultiplier = member.shield > 0 && hasSpellEffect('shielded_healing_bonus') ? 1.2 : 1
|
||||
const healing = member.hotTicks > 0 ? healAmount(member, 6, side.debuffs, healingMultiplier) : 0
|
||||
if (healing > 0) addFloatingHeal(sideName, member.id, healing)
|
||||
const nextMaxHealthPenaltyTicks = appliesMaxHealthCut && member.id === primaryTarget.id
|
||||
? 14
|
||||
@@ -842,9 +976,9 @@ export function PvPRoguelikeScreen({
|
||||
cooldowns: Object.fromEntries(
|
||||
Object.entries(side.cooldowns).map(([id, seconds]) => [id, Math.max(0, seconds - TICK_MS / 1000)]),
|
||||
),
|
||||
enemyHealth: Math.max(0, side.enemyHealth - encounterValue.partyDamage),
|
||||
enemyHealth: Math.max(0, side.enemyHealth - partyDamageOutput(nextParty, encounterValue.partyDamage)),
|
||||
}
|
||||
}, [addFloatingHeal, elapsedTicks, maxResource])
|
||||
}, [activeSpellEffects, addFloatingHeal, elapsedTicks, maxResource])
|
||||
|
||||
const beginUpgradePhase = useCallback(() => {
|
||||
setPlayerBuffChoices(chooseRandom(selfBuffChoicesCatalog, 3))
|
||||
@@ -895,12 +1029,18 @@ export function PvPRoguelikeScreen({
|
||||
addLog(`CPU ${cpuDifficulty ?? 1} fell. Finish the boss for XP.`, 'loot')
|
||||
}
|
||||
if (nextPlayer.enemyHealth <= 0) {
|
||||
if (encounter.isBoss && cpuDefeatedRef.current) {
|
||||
finishRoguelikeRun()
|
||||
setStatus('won')
|
||||
addLog('CPU defeated. Match complete.', 'loot')
|
||||
return
|
||||
}
|
||||
addLog(`${encounter.enemyName} cleared. Choose your next edge.`, 'loot')
|
||||
beginUpgradePhase()
|
||||
}
|
||||
}, TICK_MS)
|
||||
}, TICK_MS / speedMultiplier)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [addLog, advanceSide, awardBossReward, beginUpgradePhase, checkpointStage, contentType, cpuDifficulty, cpuTakeTurn, encounter, encounterIndex, encountersCleared, finishRoguelikeRun, paused, profile.character.id, stage, status])
|
||||
}, [addLog, advanceSide, awardBossReward, beginUpgradePhase, checkpointStage, contentType, cpuDifficulty, cpuTakeTurn, encounter, encounterIndex, encountersCleared, finishRoguelikeRun, paused, profile.character.id, speedMultiplier, stage, status])
|
||||
|
||||
useEffect(() => {
|
||||
if ((status !== 'won' && status !== 'lost') || recordedRunRef.current || !cpuDifficulty) return
|
||||
@@ -955,10 +1095,17 @@ export function PvPRoguelikeScreen({
|
||||
}
|
||||
|
||||
const clearedBoss = encounter.isBoss
|
||||
if (clearedBoss && cpuDefeatedRef.current) {
|
||||
finishRoguelikeRun()
|
||||
setStatus('won')
|
||||
addLog('CPU defeated. Match complete.', 'loot')
|
||||
return
|
||||
}
|
||||
const nextStage = clearedBoss ? stage + 1 : stage
|
||||
const nextSegment = clearedBoss ? buildEncounterSegment(encounterPool, nextStage, contentType) : []
|
||||
const nextEncounter = clearedBoss ? nextSegment[0] : encounters[encounterIndex + 1]
|
||||
if (!nextEncounter) {
|
||||
finishRoguelikeRun()
|
||||
setStatus('won')
|
||||
addLog('No further encounters remain.', 'loot')
|
||||
return
|
||||
@@ -1007,9 +1154,13 @@ export function PvPRoguelikeScreen({
|
||||
setElapsedTicks(0)
|
||||
setStatus('playing')
|
||||
addLog(`You chose ${selectedBuff.name} and ${selectedDebuff.name}. CPU ${cpuDifficulty} chose ${cpuBuff.name} and ${cpuDebuff.name}.`, 'system')
|
||||
}, [addLog, contentType, cpuDifficulty, encounter, encounterIndex, encounterPool, encounters, maxResource, opponentDebuffChoicesCatalog, selectedBuff, selectedDebuff, selfBuffChoicesCatalog, stage, starterSpells])
|
||||
}, [addLog, contentType, cpuDifficulty, encounter, encounterIndex, encounterPool, encounters, finishRoguelikeRun, maxResource, opponentDebuffChoicesCatalog, selectedBuff, selectedDebuff, selfBuffChoicesCatalog, stage, starterSpells])
|
||||
|
||||
useGameAction((action) => {
|
||||
if (action === 'toggleSpeed') {
|
||||
if (status === 'playing') setSpeedMultiplier((value) => (value === 1 ? 2 : 1))
|
||||
return
|
||||
}
|
||||
if (action === 'pause' || action === 'back') {
|
||||
if (status === 'playing') setPaused((value) => !value)
|
||||
return
|
||||
@@ -1036,9 +1187,9 @@ export function PvPRoguelikeScreen({
|
||||
setTargetGroup((current) => {
|
||||
const groupCount = Math.max(1, Math.ceil(playerRef.current.party.length / 6))
|
||||
const next = ((current + 1) % groupCount) as 0 | 1 | 2
|
||||
const selectedIndex = playerRef.current.party.findIndex((member) => member.id === selectedId)
|
||||
const selectedIndex = playerRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
|
||||
const nextMember = playerRef.current.party[(selectedIndex < 0 ? 0 : selectedIndex % 6) + next * 6]
|
||||
if (nextMember?.health > 0) setSelectedId(nextMember.id)
|
||||
if (nextMember?.health > 0) setSelectedTargetId(nextMember.id)
|
||||
return next
|
||||
})
|
||||
return
|
||||
@@ -1081,6 +1232,7 @@ export function PvPRoguelikeScreen({
|
||||
directPartyTargeting,
|
||||
paused,
|
||||
targetGroup,
|
||||
speedMultiplier,
|
||||
}), [
|
||||
bindings,
|
||||
controllerIconStyle,
|
||||
@@ -1105,6 +1257,7 @@ export function PvPRoguelikeScreen({
|
||||
playerSide.party,
|
||||
playerSide.resource,
|
||||
selectedId,
|
||||
speedMultiplier,
|
||||
stage,
|
||||
starterSpells,
|
||||
status,
|
||||
@@ -1128,7 +1281,7 @@ export function PvPRoguelikeScreen({
|
||||
{dualScreenEnabled && status !== 'queueing' && (
|
||||
<DualScreenTopCombat
|
||||
state={dualScreenState}
|
||||
onSelectTarget={setSelectedId}
|
||||
onSelectTarget={setSelectedTargetId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1143,6 +1296,7 @@ export function PvPRoguelikeScreen({
|
||||
<div className="resource-row pvp-resource-row">
|
||||
<div className="pvp-resource-wrap">
|
||||
<span>{gameClass.resourceName} {Math.floor(playerSide.resource)} / {maxResource}</span>
|
||||
{speedMultiplier === 2 && <strong className="speed-badge">2x speed</strong>}
|
||||
<div className="bar mana-bar"><span style={{ width: `${(playerSide.resource / maxResource) * 100}%` }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1152,7 +1306,7 @@ export function PvPRoguelikeScreen({
|
||||
<button
|
||||
className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'down' : ''}`}
|
||||
key={`player-${member.id}`}
|
||||
onClick={() => setSelectedId(member.id)}
|
||||
onClick={() => setSelectedTargetId(member.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="member-header">
|
||||
@@ -1351,9 +1505,39 @@ export function PvPRoguelikeScreen({
|
||||
<h2>{status === 'won' ? `CPU ${cpuDifficulty} Falls` : `CPU ${cpuDifficulty} Wins`}</h2>
|
||||
<p>{finalEncountersCleared} encounters cleared.</p>
|
||||
<div className="reward-summary">
|
||||
{!reward && !rewardError && <p>Boss kills grant XP immediately.</p>}
|
||||
<p>{runSummary.bossesKilled} bosses killed.</p>
|
||||
<p>+{runSummary.experienceGained} XP</p>
|
||||
{runSummary.bossesKilled > 0 && !reward && !rewardError && <p>Final boss rewards still recording...</p>}
|
||||
{rewardError && <p className="reward-error">{rewardError}</p>}
|
||||
{reward && (
|
||||
{runSummary.levelsGained > 0 && runSummary.previousLevel !== null && runSummary.newLevel !== null && (
|
||||
<p className="level-gain">
|
||||
Level {runSummary.previousLevel} to {runSummary.newLevel}
|
||||
<small>+{runSummary.talentPointsGained} talent point{runSummary.talentPointsGained === 1 ? '' : 's'}</small>
|
||||
</p>
|
||||
)}
|
||||
{runSummary.unlockedAbilities.map((ability) => (
|
||||
<p className="ability-unlock" key={ability.id}>
|
||||
<span>{ability.glyph}</span>
|
||||
Ability Unlocked: {ability.name}
|
||||
</p>
|
||||
))}
|
||||
<div className="run-loot-rolls">
|
||||
{runSummary.loot.length > 0 ? runSummary.loot.map((item, index) => (
|
||||
<div className="dropped" key={`${item.id}-${index}`}>
|
||||
<strong>Boss {index + 1}</strong>
|
||||
<span>
|
||||
{item.glyph} {item.name} x{item.quantity}
|
||||
{item.duplicate ? ` (owned x${item.quantityAfter})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
)) : (
|
||||
<div>
|
||||
<strong>Loot</strong>
|
||||
<span>No boss loot awarded</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{reward && runSummary.bossesKilled === 0 && (
|
||||
<>
|
||||
<p>+{reward.experienceGained} XP</p>
|
||||
{reward.levelsGained > 0 && (
|
||||
@@ -1392,6 +1576,7 @@ export function PvPRoguelikeScreen({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => startMatch()} type="button">Queue Next Match</button>
|
||||
<button className="secondary-result-button" onClick={onExit} type="button">Back to Roguelike</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,199 +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 [talentPage, setTalentPage] = useState(0)
|
||||
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)
|
||||
const tierPages = Array.from(
|
||||
{ length: Math.ceil(tiers.length / 2) },
|
||||
(_, index) => tiers.slice(index * 2, index * 2 + 2),
|
||||
)
|
||||
const visibleTiers = tierPages[talentPage] ?? tierPages[0] ?? []
|
||||
|
||||
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>
|
||||
|
||||
<nav className="talent-page-tabs" role="tablist" aria-label="Talent tier pages">
|
||||
{tierPages.map((pageTiers, index) => (
|
||||
<button
|
||||
aria-selected={talentPage === index}
|
||||
className={talentPage === index ? 'active' : ''}
|
||||
key={pageTiers.join('-')}
|
||||
onClick={() => setTalentPage(index)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
Tiers {pageTiers[0]}-{pageTiers[pageTiers.length - 1]}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
{!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>
|
||||
|
||||
<div className="talent-tree">
|
||||
{visibleTiers.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>
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -42,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
|
||||
@@ -54,14 +54,31 @@ export type DualScreenCombatState = {
|
||||
directPartyTargeting: boolean
|
||||
paused: boolean
|
||||
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
|
||||
@@ -102,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',
|
||||
@@ -280,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(() => {
|
||||
@@ -307,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">
|
||||
@@ -340,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>
|
||||
@@ -475,6 +582,7 @@ export function DualScreenTopCombat({
|
||||
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"
|
||||
@@ -500,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>
|
||||
|
||||
@@ -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,6 +59,9 @@ 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 },
|
||||
@@ -101,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,
|
||||
@@ -164,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)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import starterProfile from './offline-starter-profile.json'
|
||||
import { bundledCatalogHash } from './offline-catalog-meta'
|
||||
import type {
|
||||
Account,
|
||||
AuthSession,
|
||||
@@ -26,6 +27,7 @@ export interface GameRepository {
|
||||
completedPart?: number,
|
||||
startPart?: number,
|
||||
partDurationSeconds?: [number, number, number],
|
||||
hardMode?: boolean,
|
||||
): Promise<DungeonReward>
|
||||
completeRoguelike(
|
||||
dungeonId: number,
|
||||
@@ -69,6 +71,7 @@ type OfflineSave = {
|
||||
activeClassId: number
|
||||
completedDungeonParts: number
|
||||
completedRaidPhases: number
|
||||
dungeonCompletions?: Record<string, number>
|
||||
characters: Record<number, CharacterData>
|
||||
lootRolls: Record<string, LootRoll>
|
||||
}
|
||||
@@ -80,6 +83,12 @@ type OnlineCache = {
|
||||
dirty: boolean
|
||||
}
|
||||
|
||||
type CatalogCache = {
|
||||
version: 1
|
||||
hash: string
|
||||
profile: CharacterProfile
|
||||
}
|
||||
|
||||
export type CloudSyncStatus = {
|
||||
available: boolean
|
||||
dirty: boolean
|
||||
@@ -100,8 +109,11 @@ type LocalSaveStore = {
|
||||
const modeKey = 'chronicle.repositoryMode'
|
||||
const offlineSaveKey = 'chronicle.offlineSave.v1'
|
||||
const onlineCacheKey = 'chronicle.onlineCache.v1'
|
||||
const catalogCacheKey = 'chronicle.catalog.v1'
|
||||
const catalogBundleKey = 'chronicle.catalog.bundleHash.v1'
|
||||
const authTokenKey = 'chronicle.authToken.v1'
|
||||
const offlineAccount = { id: -1, username: 'Offline' }
|
||||
const ABILITY_SLOT_COUNT = 6
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return structuredClone(value)
|
||||
@@ -146,7 +158,7 @@ function upgradeV1Save(v1: { profile: CharacterProfile; lootRolls: Record<string
|
||||
level: cid === p.character.classId ? p.character.level : 1,
|
||||
experience: cid === p.character.classId ? p.character.experience : 0,
|
||||
talentPoints: cid === p.character.classId ? p.character.talentPoints : 1,
|
||||
abilitySlots: cid === p.character.classId ? [...p.abilitySlots] : [],
|
||||
abilitySlots: cid === p.character.classId ? normalizeAbilitySlots(p.abilitySlots) : [],
|
||||
talentRanks,
|
||||
inventory: cid === p.character.classId ? clone(p.inventory) : [],
|
||||
}
|
||||
@@ -157,17 +169,41 @@ function upgradeV1Save(v1: { profile: CharacterProfile; lootRolls: Record<string
|
||||
activeClassId: p.character.classId,
|
||||
completedDungeonParts: p.completedDungeonParts,
|
||||
completedRaidPhases: p.completedRaidPhases ?? 0,
|
||||
dungeonCompletions: Object.fromEntries(
|
||||
p.dungeons.map((dungeon) => [String(dungeon.id), dungeon.completionCount ?? 0]),
|
||||
),
|
||||
characters,
|
||||
lootRolls: v1.lootRolls ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
function upgradeV2Save(v2: Omit<OfflineSave, 'version' | 'completedRaidPhases'> & { version: 2 }): OfflineSave {
|
||||
return {
|
||||
return normalizeSaveAbilitySlots({
|
||||
...v2,
|
||||
version: 3,
|
||||
completedRaidPhases: 0,
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeAbilitySlots(abilitySlots: unknown): Array<number | null> {
|
||||
const slots = Array.isArray(abilitySlots)
|
||||
? abilitySlots
|
||||
.slice(0, ABILITY_SLOT_COUNT)
|
||||
.map((value) => {
|
||||
if (value === null || value === undefined) return null
|
||||
const id = Number(value)
|
||||
return Number.isInteger(id) ? id : null
|
||||
})
|
||||
: []
|
||||
while (slots.length < ABILITY_SLOT_COUNT) slots.push(null)
|
||||
return slots
|
||||
}
|
||||
|
||||
function normalizeSaveAbilitySlots(save: OfflineSave): OfflineSave {
|
||||
for (const character of Object.values(save.characters)) {
|
||||
character.abilitySlots = normalizeAbilitySlots(character.abilitySlots)
|
||||
}
|
||||
return save
|
||||
}
|
||||
|
||||
function normalizeOfflineSave(raw: unknown): OfflineSave | null {
|
||||
@@ -177,12 +213,12 @@ function normalizeOfflineSave(raw: unknown): OfflineSave | null {
|
||||
profile?: CharacterProfile
|
||||
lootRolls?: Record<string, LootRoll>
|
||||
}
|
||||
if (candidate.version === 3) return candidate as OfflineSave
|
||||
if (candidate.version === 3) return normalizeSaveAbilitySlots(candidate as OfflineSave)
|
||||
if (candidate.version === 2) {
|
||||
return upgradeV2Save(candidate as Omit<OfflineSave, 'version' | 'completedRaidPhases'> & { version: 2 })
|
||||
}
|
||||
if (candidate.version === 1 && candidate.profile) {
|
||||
return upgradeV1Save(candidate as { profile: CharacterProfile; lootRolls: Record<string, LootRoll> })
|
||||
return normalizeSaveAbilitySlots(upgradeV1Save(candidate as { profile: CharacterProfile; lootRolls: Record<string, LootRoll> }))
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -254,8 +290,42 @@ function clearOnlineCache() {
|
||||
localStorage.removeItem(onlineCacheKey)
|
||||
}
|
||||
|
||||
function bundledCatalog(): CatalogCache {
|
||||
return {
|
||||
version: 1,
|
||||
hash: bundledCatalogHash,
|
||||
profile: starterProfile as CharacterProfile,
|
||||
}
|
||||
}
|
||||
|
||||
function readCatalogCache(): CatalogCache | null {
|
||||
if (localStorage.getItem(catalogBundleKey) !== bundledCatalogHash) {
|
||||
localStorage.removeItem(catalogCacheKey)
|
||||
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
|
||||
return null
|
||||
}
|
||||
const serialized = localStorage.getItem(catalogCacheKey)
|
||||
if (!serialized) return null
|
||||
try {
|
||||
const raw = JSON.parse(serialized) as CatalogCache
|
||||
if (raw.version !== 1 || typeof raw.hash !== 'string' || !raw.profile) return null
|
||||
return raw
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeCatalogCache(cache: CatalogCache) {
|
||||
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
|
||||
localStorage.setItem(catalogCacheKey, JSON.stringify(cache))
|
||||
}
|
||||
|
||||
function activeCatalog(): CatalogCache {
|
||||
return readCatalogCache() ?? bundledCatalog()
|
||||
}
|
||||
|
||||
function buildProfile(save: OfflineSave): CharacterProfile {
|
||||
const static_ = clone(starterProfile) as CharacterProfile
|
||||
const static_ = clone(activeCatalog().profile)
|
||||
const cd = save.characters[save.activeClassId]
|
||||
const gameClass = static_.classes.find((c) => c.id === save.activeClassId)!
|
||||
|
||||
@@ -291,6 +361,10 @@ function buildProfile(save: OfflineSave): CharacterProfile {
|
||||
updateCraftingRecipes(static_)
|
||||
static_.completedDungeonParts = save.completedDungeonParts
|
||||
static_.completedRaidPhases = save.completedRaidPhases
|
||||
static_.dungeons = static_.dungeons.map((dungeon) => ({
|
||||
...dungeon,
|
||||
completionCount: save.dungeonCompletions?.[String(dungeon.id)] ?? dungeon.completionCount ?? 0,
|
||||
}))
|
||||
|
||||
return static_
|
||||
}
|
||||
@@ -333,10 +407,13 @@ function updateCraftingRecipes(profile: CharacterProfile) {
|
||||
...component,
|
||||
owned: owned.get(component.item.id) ?? 0,
|
||||
}))
|
||||
const hasRequiredComponents = components.length > 0
|
||||
&& components.every((component) => component.quantity > 0)
|
||||
return {
|
||||
...recipe,
|
||||
components,
|
||||
canCraft: components.every((component) => component.owned >= component.quantity),
|
||||
canCraft: hasRequiredComponents
|
||||
&& components.every((component) => component.owned >= component.quantity),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -355,15 +432,58 @@ function addInventoryItem(inventory: Item[], item: Omit<Item, 'quantity' | 'equi
|
||||
return { duplicate: false, quantityAfter: quantity }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function experienceForLevel(level: number) {
|
||||
return (level - 1) * (level - 1) * 100
|
||||
}
|
||||
|
||||
function catchUpExperienceReward(
|
||||
baseReward: number,
|
||||
currentExperience: number,
|
||||
currentLevel: number,
|
||||
targetLevel: number,
|
||||
) {
|
||||
if (targetLevel <= currentLevel) return baseReward
|
||||
const targetExperience = experienceForLevel(targetLevel)
|
||||
const gap = Math.max(0, targetExperience - currentExperience)
|
||||
if (gap <= 0) return baseReward
|
||||
const doubledBase = Math.min(baseReward, Math.ceil(gap / 2))
|
||||
return doubledBase * 2 + (baseReward - doubledBase)
|
||||
}
|
||||
|
||||
function highestOtherClassLevel(save: OfflineSave) {
|
||||
const activeClass = save.activeClassId
|
||||
return Object.entries(save.characters)
|
||||
.filter(([classId]) => Number(classId) !== activeClass)
|
||||
.reduce((highest, [, character]) => Math.max(highest, character.level), 0)
|
||||
}
|
||||
|
||||
function scaledPvpBossExperience(
|
||||
startingExperience: number,
|
||||
startingLevel: number,
|
||||
bossesCleared: number,
|
||||
maxLevel: number,
|
||||
targetLevel = startingLevel,
|
||||
) {
|
||||
let experience = startingExperience
|
||||
let level = startingLevel
|
||||
@@ -374,7 +494,8 @@ function scaledPvpBossExperience(
|
||||
? maxExperience
|
||||
: experienceForLevel(level + 1)
|
||||
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
|
||||
experience = Math.min(maxExperience, experience + Math.round(levelBand * 0.25))
|
||||
const rewardRate = targetLevel > level ? 0.5 : 0.25
|
||||
experience = Math.min(maxExperience, experience + Math.round(levelBand * rewardRate))
|
||||
while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
|
||||
level += 1
|
||||
}
|
||||
@@ -382,15 +503,25 @@ function scaledPvpBossExperience(
|
||||
return { experience, level }
|
||||
}
|
||||
|
||||
function talentEffectCapacity(level: number) {
|
||||
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
||||
}
|
||||
|
||||
function talentEffectSource(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
|
||||
}
|
||||
|
||||
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
|
||||
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
|
||||
1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
|
||||
5: { id: 601, slug: 'basic-component', name: 'Basic Component', itemLevel: 5, glyph: '◇', description: 'A standard crafting component.' },
|
||||
10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' },
|
||||
15: { id: 603, slug: 'advanced-component', name: 'Advanced Component', itemLevel: 15, glyph: '◉', description: 'An advanced crafting component.' },
|
||||
20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' },
|
||||
25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' },
|
||||
}
|
||||
const DIRECT_CRAFT_ITEM_LEVELS = new Set([1, 10, 20, 25])
|
||||
|
||||
type WindowWithApiBase = Window & {
|
||||
CAPACITOR_API_BASE_URL?: string
|
||||
@@ -425,7 +556,7 @@ function mergeProfileIntoSave(profile: CharacterProfile, existingSave?: OfflineS
|
||||
level: profile.character.level,
|
||||
experience: profile.character.experience,
|
||||
talentPoints: profile.character.talentPoints,
|
||||
abilitySlots: [...profile.abilitySlots],
|
||||
abilitySlots: normalizeAbilitySlots(profile.abilitySlots),
|
||||
talentRanks,
|
||||
inventory: clone(profile.inventory),
|
||||
}
|
||||
@@ -435,6 +566,9 @@ function mergeProfileIntoSave(profile: CharacterProfile, existingSave?: OfflineS
|
||||
activeClassId: profile.character.classId,
|
||||
completedDungeonParts: profile.completedDungeonParts,
|
||||
completedRaidPhases: profile.completedRaidPhases,
|
||||
dungeonCompletions: Object.fromEntries(
|
||||
profile.dungeons.map((dungeon) => [String(dungeon.id), dungeon.completionCount ?? 0]),
|
||||
),
|
||||
characters,
|
||||
lootRolls: clone(existingSave?.lootRolls ?? {}),
|
||||
}
|
||||
@@ -566,6 +700,23 @@ function isNetworkError(reason: unknown): reason is NetworkError {
|
||||
return reason instanceof Error && Boolean((reason as NetworkError).network)
|
||||
}
|
||||
|
||||
async function loadServerCatalog(): Promise<CatalogCache> {
|
||||
return requestJson('/api/catalog')
|
||||
}
|
||||
|
||||
async function refreshCatalogFromServer(): Promise<CatalogCache | null> {
|
||||
try {
|
||||
const catalog = await loadServerCatalog()
|
||||
if (catalog.version !== 1 || !catalog.hash || !catalog.profile) return null
|
||||
if (catalog.hash !== activeCatalog().hash || !readCatalogCache()) {
|
||||
writeCatalogCache(catalog)
|
||||
}
|
||||
return catalog
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function cachedOnlineSession(): AuthSession | null {
|
||||
const cache = readOnlineCache()
|
||||
if (!cache) return null
|
||||
@@ -607,6 +758,7 @@ async function pushServerSyncSave(save: OfflineSave): Promise<{ profile: Charact
|
||||
async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
|
||||
const cache = readOnlineCache()
|
||||
if (session.token) writeAuthToken(session.token)
|
||||
await refreshCatalogFromServer()
|
||||
if (!session.account || !session.profile) {
|
||||
if (session.account && cache?.account.id === session.account.id) {
|
||||
return {
|
||||
@@ -718,7 +870,7 @@ const serverRepository: GameRepository = {
|
||||
),
|
||||
saveProfile: (classId, abilitySlots) =>
|
||||
cachedOnlineLocalRepository.saveProfile(classId, abilitySlots),
|
||||
completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) =>
|
||||
completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds, hardMode) =>
|
||||
cachedOnlineLocalRepository.completeDungeon(
|
||||
dungeonId,
|
||||
difficultyId,
|
||||
@@ -727,6 +879,7 @@ const serverRepository: GameRepository = {
|
||||
completedPart,
|
||||
startPart,
|
||||
partDurationSeconds,
|
||||
hardMode,
|
||||
),
|
||||
completeRoguelike: (dungeonId, difficultyId, encountersCleared, resourceSpent, durationSeconds, options) =>
|
||||
cachedOnlineLocalRepository.completeRoguelike(
|
||||
@@ -756,17 +909,16 @@ const serverRepository: GameRepository = {
|
||||
}
|
||||
|
||||
function emptyCharacterData(classId: number): CharacterData {
|
||||
const static_ = clone(starterProfile) as CharacterProfile
|
||||
const static_ = clone(activeCatalog().profile)
|
||||
const gc = static_.classes.find((c) => c.id === classId)!
|
||||
const talentRanks: Record<string, number> = {}
|
||||
for (const t of gc.talents) talentRanks[String(t.id)] = 0
|
||||
const starterItemIds = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
|
||||
const inventory: Item[] = static_.inventory.filter((i) => starterItemIds.includes(i.id)).map((i) => ({ ...i }))
|
||||
const inventory: Item[] = []
|
||||
const startingAbilitySlots: Array<number | null> = gc.spells
|
||||
.filter((s) => s.unlockLevel === 1)
|
||||
.slice(0, 5)
|
||||
.slice(0, ABILITY_SLOT_COUNT)
|
||||
.map((s) => s.id)
|
||||
while (startingAbilitySlots.length < 6) startingAbilitySlots.push(null)
|
||||
while (startingAbilitySlots.length < ABILITY_SLOT_COUNT) startingAbilitySlots.push(null)
|
||||
return {
|
||||
level: 1,
|
||||
experience: 0,
|
||||
@@ -806,35 +958,34 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
},
|
||||
async saveProfile(classId, abilitySlots) {
|
||||
const save = requireStoredSave(store)
|
||||
const static_ = clone(starterProfile) as CharacterProfile
|
||||
const gameClass = static_.classes.find((candidate) => candidate.id === classId)
|
||||
if (!gameClass) throw new Error('Selected class does not exist.')
|
||||
const static_ = clone(starterProfile) as CharacterProfile
|
||||
const gameClass = static_.classes.find((candidate) => candidate.id === classId)
|
||||
if (!gameClass) throw new Error('Selected class does not exist.')
|
||||
|
||||
const slots = abilitySlots.slice(0, 6)
|
||||
while (slots.length < 6) slots.push(null)
|
||||
const selectedIds = slots.filter((id): id is number => id !== null)
|
||||
if (new Set(selectedIds).size !== selectedIds.length) {
|
||||
throw new Error('The same ability cannot be equipped twice.')
|
||||
}
|
||||
const activeChar = save.characters[save.activeClassId]
|
||||
const validIds = new Set(
|
||||
gameClass.spells
|
||||
.filter((spell) => spell.unlockLevel <= activeChar.level)
|
||||
.map((spell) => spell.id),
|
||||
)
|
||||
if (selectedIds.some((id) => !validIds.has(id))) {
|
||||
throw new Error('One or more abilities are locked or belong to another class.')
|
||||
}
|
||||
const slots = normalizeAbilitySlots(abilitySlots)
|
||||
const selectedIds = slots.filter((id): id is number => id !== null)
|
||||
if (new Set(selectedIds).size !== selectedIds.length) {
|
||||
throw new Error('The same ability cannot be equipped twice.')
|
||||
}
|
||||
const activeChar = save.characters[save.activeClassId]
|
||||
const validIds = new Set(
|
||||
gameClass.spells
|
||||
.filter((spell) => spell.unlockLevel <= activeChar.level)
|
||||
.map((spell) => spell.id),
|
||||
)
|
||||
if (selectedIds.some((id) => !validIds.has(id))) {
|
||||
throw new Error('One or more abilities are locked or belong to another class.')
|
||||
}
|
||||
|
||||
if (!save.characters[classId]) {
|
||||
save.characters[classId] = emptyCharacterData(classId)
|
||||
}
|
||||
save.characters[classId].abilitySlots = slots
|
||||
save.activeClassId = classId
|
||||
if (!save.characters[classId]) {
|
||||
save.characters[classId] = emptyCharacterData(classId)
|
||||
}
|
||||
save.characters[classId].abilitySlots = slots
|
||||
save.activeClassId = classId
|
||||
store.writeSave(save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async completeDungeon(dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) {
|
||||
async completeDungeon(dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds, hardMode) {
|
||||
void startPart
|
||||
void partDurationSeconds
|
||||
if (!Number.isInteger(resourceSpent) || resourceSpent < 0) {
|
||||
@@ -860,8 +1011,15 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const previousLevel = cd.level
|
||||
const previousExperience = cd.experience
|
||||
const partCount = completedPart ?? 1
|
||||
const experienceReward = Math.round(
|
||||
dungeon.experienceReward * difficulty.experienceMultiplier * partCount,
|
||||
const rewardMultiplier = hardMode ? 2 : 1
|
||||
const baseExperienceReward = Math.round(
|
||||
dungeon.experienceReward * difficulty.experienceMultiplier * partCount * rewardMultiplier,
|
||||
)
|
||||
const experienceReward = catchUpExperienceReward(
|
||||
baseExperienceReward,
|
||||
previousExperience,
|
||||
previousLevel,
|
||||
highestOtherClassLevel(save),
|
||||
)
|
||||
const maxExperience = experienceForLevel(profile.maxLevel)
|
||||
const newExperience = Math.min(previousExperience + experienceReward, maxExperience)
|
||||
@@ -896,6 +1054,10 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
} else {
|
||||
save.completedDungeonParts = Math.max(save.completedDungeonParts, partCount)
|
||||
}
|
||||
save.dungeonCompletions = {
|
||||
...(save.dungeonCompletions ?? {}),
|
||||
[String(dungeonId)]: (save.dungeonCompletions?.[String(dungeonId)] ?? 0) + 1,
|
||||
}
|
||||
|
||||
let bonusItem: DungeonReward['bonusItem'] = null
|
||||
if ((startPart ?? 1) === 1 && partCount >= 3 && dungeon.completionLoot.length > 0) {
|
||||
@@ -909,19 +1071,20 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const selected = rewardPool[Math.floor(Math.random() * rewardPool.length)]
|
||||
const existing = profile.inventory.find((item) => item.id === selected.id)
|
||||
const duplicate = Boolean(existing)
|
||||
let quantityAfter = 1
|
||||
const rewardQuantity = rewardMultiplier
|
||||
let quantityAfter = rewardQuantity
|
||||
if (existing) {
|
||||
existing.quantity += 1
|
||||
existing.quantity += rewardQuantity
|
||||
quantityAfter = existing.quantity
|
||||
} else {
|
||||
profile.inventory.push({
|
||||
...selected,
|
||||
quantity: 1,
|
||||
quantity: rewardQuantity,
|
||||
equipped: false,
|
||||
})
|
||||
}
|
||||
cd.inventory = profile.inventory
|
||||
bonusItem = { ...selected, quantity: 1, duplicate, quantityAfter }
|
||||
bonusItem = { ...selected, quantity: rewardQuantity, duplicate, quantityAfter }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,13 +1137,19 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const maxExperience = experienceForLevel(profile.maxLevel)
|
||||
const bossesCleared = Math.max(0, Math.floor(options?.bossesCleared ?? encountersCleared / 3))
|
||||
const scaledReward = options?.experienceMode === 'pvp-boss-quarter-level'
|
||||
? scaledPvpBossExperience(previousExperience, previousLevel, bossesCleared, profile.maxLevel)
|
||||
? scaledPvpBossExperience(previousExperience, previousLevel, bossesCleared, profile.maxLevel, highestOtherClassLevel(save))
|
||||
: null
|
||||
const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3))
|
||||
const newExperience = scaledReward
|
||||
? scaledReward.experience
|
||||
: Math.min(
|
||||
previousExperience
|
||||
+ Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3)),
|
||||
+ catchUpExperienceReward(
|
||||
baseRoguelikeReward,
|
||||
previousExperience,
|
||||
previousLevel,
|
||||
highestOtherClassLevel(save),
|
||||
),
|
||||
maxExperience,
|
||||
)
|
||||
let newLevel = scaledReward?.level ?? previousLevel
|
||||
@@ -1044,6 +1213,34 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
)!
|
||||
const talent = gameClass.talents.find((candidate) => candidate.id === talentId)
|
||||
if (!talent) throw new Error('That talent does not belong to the active class.')
|
||||
if (save.activeClassId === 1) {
|
||||
if ((cd.talentRanks[String(talentId)] ?? 0) > 0) {
|
||||
cd.talentRanks[String(talentId)] = 0
|
||||
} else {
|
||||
const capacity = talentEffectCapacity(cd.level)
|
||||
if (capacity <= 0) throw new Error('Spell effects unlock at level 5.')
|
||||
const source = talentEffectSource(talent.effectType)
|
||||
const sourceConflict = gameClass.talents.find(
|
||||
(candidate) =>
|
||||
candidate.id !== talentId
|
||||
&& (cd.talentRanks[String(candidate.id)] ?? 0) > 0
|
||||
&& talentEffectSource(candidate.effectType) === source,
|
||||
)
|
||||
if (sourceConflict) {
|
||||
throw new Error(`Only one ${source} spell effect can be active.`)
|
||||
}
|
||||
const activeCount = gameClass.talents.reduce(
|
||||
(total, candidate) => total + ((cd.talentRanks[String(candidate.id)] ?? 0) > 0 ? 1 : 0),
|
||||
0,
|
||||
)
|
||||
if (activeCount >= capacity) {
|
||||
throw new Error(`Level ${cd.level} allows ${capacity} active spell effect${capacity === 1 ? '' : 's'}.`)
|
||||
}
|
||||
cd.talentRanks[String(talentId)] = 1
|
||||
}
|
||||
store.writeSave(save)
|
||||
return buildProfile(save)
|
||||
}
|
||||
if (cd.talentPoints <= 0) {
|
||||
throw new Error('No talent points are available.')
|
||||
}
|
||||
@@ -1086,10 +1283,12 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
for (const talent of gameClass.talents) {
|
||||
cd.talentRanks[String(talent.id)] = 0
|
||||
}
|
||||
cd.talentPoints = Math.min(
|
||||
profile.maxTalentPoints,
|
||||
cd.talentPoints + refunded,
|
||||
)
|
||||
if (save.activeClassId !== 1) {
|
||||
cd.talentPoints = Math.min(
|
||||
profile.maxTalentPoints,
|
||||
cd.talentPoints + refunded,
|
||||
)
|
||||
}
|
||||
store.writeSave(save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
@@ -1164,12 +1363,16 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const profile = buildProfile(save)
|
||||
const recipe = profile.craftingRecipes.find((candidate) => candidate.id === recipeId)
|
||||
if (!recipe) throw new Error('That crafting recipe does not exist.')
|
||||
const requiresUpgrade = !DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel)
|
||||
if (requiresUpgrade) throw new Error('Upgrade the previous item tier instead.')
|
||||
if (recipe.components.length === 0) throw new Error('That recipe has no component requirements.')
|
||||
const missing = recipe.components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`)
|
||||
}
|
||||
|
||||
for (const component of recipe.components) {
|
||||
if (component.quantity <= 0) throw new Error('Recipe components must require at least one item.')
|
||||
const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
|
||||
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to craft this item.`)
|
||||
owned.quantity -= component.quantity
|
||||
@@ -1191,19 +1394,17 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
if (item.slot === 'component') throw new Error('Components cannot be upgraded.')
|
||||
const currentRecipe = profile.craftingRecipes.find((recipe) => recipe.item.id === item.id)
|
||||
const targetRecipe = currentRecipe
|
||||
? profile.craftingRecipes.find((recipe) =>
|
||||
recipe.sourceEncounterId === currentRecipe.sourceEncounterId
|
||||
&& recipe.item.slot === item.slot
|
||||
&& recipe.item.itemLevel === item.itemLevel + 5,
|
||||
)
|
||||
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, item)
|
||||
: null
|
||||
if (!targetRecipe) throw new Error('No upgrade is available for this item.')
|
||||
if (targetRecipe.components.length === 0) throw new Error('That upgrade has no component requirements.')
|
||||
const missing = targetRecipe.components.find((component) => component.owned < component.quantity)
|
||||
if (missing) {
|
||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`)
|
||||
}
|
||||
|
||||
for (const component of targetRecipe.components) {
|
||||
if (component.quantity <= 0) throw new Error('Upgrade components must require at least one item.')
|
||||
const owned = profile.inventory.find((candidate) => candidate.id === component.item.id)
|
||||
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to upgrade this item.`)
|
||||
owned.quantity -= component.quantity
|
||||
@@ -1356,7 +1557,7 @@ const cachedOnlineRepository: GameRepository = {
|
||||
},
|
||||
loadProfile: () => cachedOnlineLocalRepository.loadProfile(),
|
||||
saveProfile: (classId, abilitySlots) => cachedOnlineLocalRepository.saveProfile(classId, abilitySlots),
|
||||
completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds) =>
|
||||
completeDungeon: (dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds, hardMode) =>
|
||||
cachedOnlineLocalRepository.completeDungeon(
|
||||
dungeonId,
|
||||
difficultyId,
|
||||
@@ -1365,6 +1566,7 @@ const cachedOnlineRepository: GameRepository = {
|
||||
completedPart,
|
||||
startPart,
|
||||
partDurationSeconds,
|
||||
hardMode,
|
||||
),
|
||||
completeRoguelike: (dungeonId, difficultyId, encountersCleared, resourceSpent, durationSeconds, options) =>
|
||||
cachedOnlineLocalRepository.completeRoguelike(
|
||||
@@ -1403,7 +1605,9 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
||||
if (!cache) {
|
||||
throw new Error('No signed-in save is available for cloud sync.')
|
||||
}
|
||||
await refreshCatalogFromServer()
|
||||
const synced = await pushServerSyncSave(cache.save)
|
||||
await refreshCatalogFromServer()
|
||||
writeOnlineCache({
|
||||
version: 1,
|
||||
account: cache.account,
|
||||
@@ -1411,7 +1615,7 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
||||
dirty: false,
|
||||
})
|
||||
writeMode('online')
|
||||
return synced.profile
|
||||
return buildProfile(synced.save)
|
||||
}
|
||||
|
||||
export function selectOnlineMode() {
|
||||
|
||||
@@ -35,6 +35,7 @@ export const INPUT_ACTIONS = [
|
||||
'targetParty5',
|
||||
'targetParty6',
|
||||
'toggleTargetGroup',
|
||||
'toggleSpeed',
|
||||
'pause',
|
||||
] as const
|
||||
|
||||
@@ -63,6 +64,7 @@ export const ACTION_LABELS: Record<InputAction, string> = {
|
||||
targetParty5: 'Target Party Member 5',
|
||||
targetParty6: 'Target Party Member 6',
|
||||
toggleTargetGroup: 'Switch Raid Target Group',
|
||||
toggleSpeed: 'Toggle 2x Speed',
|
||||
pause: 'Pause Menu',
|
||||
}
|
||||
|
||||
@@ -89,6 +91,7 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
|
||||
targetParty5: 'F5',
|
||||
targetParty6: 'F6',
|
||||
toggleTargetGroup: 'Tab',
|
||||
toggleSpeed: 'Backquote',
|
||||
pause: 'Escape',
|
||||
},
|
||||
controller: {
|
||||
@@ -111,8 +114,9 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
|
||||
targetParty3: 'Button15',
|
||||
targetParty4: 'Button13',
|
||||
targetParty5: 'Button4',
|
||||
targetParty6: 'Button11',
|
||||
targetParty6: 'Button10',
|
||||
toggleTargetGroup: 'Button6',
|
||||
toggleSpeed: 'Button11',
|
||||
pause: 'Button9',
|
||||
},
|
||||
}
|
||||
@@ -145,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',
|
||||
@@ -166,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,
|
||||
@@ -276,14 +290,6 @@ function hasUiOverlay() {
|
||||
).some(isVisible)
|
||||
}
|
||||
|
||||
function isCombatTargetAction(action: InputAction) {
|
||||
return action.startsWith('navigate')
|
||||
|| action.startsWith('targetParty')
|
||||
|| action === 'previousTarget'
|
||||
|| action === 'nextTarget'
|
||||
|| action === 'toggleTargetGroup'
|
||||
}
|
||||
|
||||
const BUTTON_LABELS: Record<number, string> = {
|
||||
0: 'A / Cross',
|
||||
1: 'B / Circle',
|
||||
@@ -397,7 +403,6 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
const keyboardInputRef = useRef(keyboardInput)
|
||||
const previousTokensRef = useRef(new Set<string>())
|
||||
const repeatRef = useRef<Record<string, number>>({})
|
||||
const lastCombatNavigationRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
bindingsRef.current = bindings
|
||||
@@ -444,11 +449,6 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
const dispatchAction = useCallback((action: InputAction, device: InputDevice) => {
|
||||
const uiOverlay = hasUiOverlay()
|
||||
const combatActive = Boolean(document.querySelector('[data-combat-active="true"]'))
|
||||
if (combatActive && !uiOverlay && isCombatTargetAction(action)) {
|
||||
const now = performance.now()
|
||||
if (now - lastCombatNavigationRef.current < 125) return
|
||||
lastCombatNavigationRef.current = now
|
||||
}
|
||||
|
||||
setLastDevice(device)
|
||||
document.documentElement.dataset.inputDevice = device
|
||||
@@ -518,9 +518,11 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
'targetParty5',
|
||||
'targetParty6',
|
||||
'toggleTargetGroup',
|
||||
'toggleSpeed',
|
||||
] satisfies InputAction[]
|
||||
const combatPriority = [
|
||||
'pause',
|
||||
'toggleSpeed',
|
||||
'ability1',
|
||||
'ability2',
|
||||
'ability3',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const bundledCatalogHash = '07506c52bab428c439f09a9b82e39e6eff2b243fb972d664da48852059bcd937'
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -319,6 +327,7 @@ export async function completeDungeon(
|
||||
completedPart?: number,
|
||||
startPart?: number,
|
||||
partDurationSeconds?: [number, number, number],
|
||||
hardMode?: boolean,
|
||||
): Promise<DungeonReward> {
|
||||
return activeGameRepository().completeDungeon(
|
||||
dungeonId,
|
||||
@@ -328,6 +337,7 @@ export async function completeDungeon(
|
||||
completedPart,
|
||||
startPart,
|
||||
partDurationSeconds,
|
||||
hardMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||