Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1e2c6d8b5 | |||
| f7b041f86f | |||
| 05bd70a9fe | |||
| bb5c7e6e21 | |||
| 14bec979e6 | |||
| 4b45483ac3 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 63
|
versionCode 69
|
||||||
versionName "1.0.43"
|
versionName "1.0.50"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
+89
-44
@@ -455,17 +455,17 @@ INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, d
|
|||||||
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 (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 (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) VALUES (2003, 2283101, 101, 100, 1);
|
||||||
INSERT INTO encounter_loot (encounter_id, item_id, difficulty_id, drop_weight, drop_chance) VALUES (2103, 2286101, 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) VALUES (2203, 2289101, 101, 100, 1);
|
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) VALUES (2303, 783103, 103, 100, 1);
|
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) VALUES (2403, 786103, 103, 100, 1);
|
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) VALUES (2503, 789103, 103, 100, 1);
|
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) VALUES (2603, 983104, 104, 100, 1);
|
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) VALUES (2703, 986104, 104, 100, 1);
|
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) VALUES (2803, 989104, 104, 100, 1);
|
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) VALUES (2903, 1183105, 105, 100, 1);
|
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) VALUES (3003, 1186105, 105, 100, 1);
|
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) VALUES (3103, 1189105, 105, 100, 1);
|
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 = 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 = 1002;
|
||||||
@@ -532,42 +532,87 @@ INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (10
|
|||||||
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 (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 (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 (1009, 583001, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 683002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 383002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 683002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 683002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 683002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 383002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 783002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 683002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 783002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 383002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 783002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 683002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 483002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 783002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 10);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 483002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 1283004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 783002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 1283004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 483002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 1283004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 783002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1304, 1383004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 583002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1305, 1383004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1306, 1383004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 583002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1307, 1483004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1308, 1483004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 583002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1309, 1483004, 20);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 5);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1401, 1583005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 683003, 7);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1402, 1583005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 983003, 8);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1403, 1583005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 683003, 7);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1404, 1683005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 983003, 8);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1405, 1683005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 683003, 7);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1406, 1683005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 983003, 8);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1407, 1783005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 783003, 7);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1408, 1783005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 1083003, 8);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1409, 1783005, 25);
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (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 (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 (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) VALUES (2003, 2283101, 10);
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (2004, 2286101, 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) VALUES (2005, 2286101, 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) VALUES (2006, 2286101, 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) VALUES (2007, 2289101, 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) VALUES (2008, 2289101, 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) VALUES (2009, 2289101, 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;
|
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 (1, 201);
|
||||||
|
|||||||
+15
@@ -1908,6 +1908,21 @@ JOIN coin_sources
|
|||||||
ON coin_sources.encounter_id = crafting_recipes.source_encounter_id
|
ON coin_sources.encounter_id = crafting_recipes.source_encounter_id
|
||||||
AND coin_sources.difficulty_id = crafting_recipes.difficulty_id;
|
AND coin_sources.difficulty_id = crafting_recipes.difficulty_id;
|
||||||
|
|
||||||
|
DELETE FROM crafting_recipe_components
|
||||||
|
WHERE recipe_id IN (1101, 1102, 1103);
|
||||||
|
|
||||||
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity)
|
||||||
|
SELECT recipe_id, items.id, quantity
|
||||||
|
FROM (
|
||||||
|
SELECT 1101 AS recipe_id, 'tigrex-boss-coin-diff-2-ilvl-10' AS item_slug, 5 AS quantity
|
||||||
|
UNION ALL SELECT 1101, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
|
||||||
|
UNION ALL SELECT 1102, 'tigrex-boss-coin-diff-2-ilvl-10', 5
|
||||||
|
UNION ALL SELECT 1102, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
|
||||||
|
UNION ALL SELECT 1103, 'tigrex-boss-coin-diff-2-ilvl-10', 5
|
||||||
|
UNION ALL SELECT 1103, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
|
||||||
|
) AS requirements
|
||||||
|
JOIN items ON items.slug = requirements.item_slug;
|
||||||
|
|
||||||
DELETE FROM gear_upgrade_paths;
|
DELETE FROM gear_upgrade_paths;
|
||||||
|
|
||||||
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id)
|
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { readFileSync, writeFileSync } from 'node:fs'
|
import { readFileSync, writeFileSync } from 'node:fs'
|
||||||
import { DatabaseSync } from 'node:sqlite'
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
|
import { catalogPayload } from '../server/catalog.mjs'
|
||||||
import { getProfile } from '../server/game-api.mjs'
|
import { getProfile } from '../server/game-api.mjs'
|
||||||
|
|
||||||
const database = new DatabaseSync(':memory:')
|
const database = new DatabaseSync(':memory:')
|
||||||
@@ -7,10 +8,14 @@ const database = new DatabaseSync(':memory:')
|
|||||||
try {
|
try {
|
||||||
database.exec(readFileSync('db/schema.sql', 'utf8'))
|
database.exec(readFileSync('db/schema.sql', 'utf8'))
|
||||||
database.exec(readFileSync('db/seed.sql', 'utf8'))
|
database.exec(readFileSync('db/seed.sql', 'utf8'))
|
||||||
const profile = getProfile(database, 1)
|
const catalog = catalogPayload(getProfile(database, 1))
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
'src/offline-starter-profile.json',
|
'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.')
|
console.log('Offline starter profile exported from SQLite.')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+33
-1
@@ -375,6 +375,31 @@ const server = createServer(async (request, response) => {
|
|||||||
SELECT from_item_id AS fromItemId, to_item_id AS toItemId
|
SELECT from_item_id AS fromItemId, to_item_id AS toItemId
|
||||||
FROM gear_upgrade_paths ORDER BY from_item_id
|
FROM gear_upgrade_paths ORDER BY from_item_id
|
||||||
`).all()
|
`).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, {
|
sendJson(response, 200, {
|
||||||
items,
|
items,
|
||||||
encounters,
|
encounters,
|
||||||
@@ -383,6 +408,11 @@ const server = createServer(async (request, response) => {
|
|||||||
craftingRecipes: [...recipes.values()],
|
craftingRecipes: [...recipes.values()],
|
||||||
dungeons,
|
dungeons,
|
||||||
gearUpgradePaths,
|
gearUpgradePaths,
|
||||||
|
classes: classes.map((gameClass) => ({
|
||||||
|
...gameClass,
|
||||||
|
abilities: abilities.filter((ability) => ability.classId === gameClass.id),
|
||||||
|
talents: talents.filter((talent) => talent.classId === gameClass.id),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -499,12 +529,14 @@ const server = createServer(async (request, response) => {
|
|||||||
const recipeComponents = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components$/)
|
const recipeComponents = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components$/)
|
||||||
if (recipeComponents && request.method === 'POST') {
|
if (recipeComponents && request.method === 'POST') {
|
||||||
const payload = await readJson(request)
|
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(`
|
database.prepare(`
|
||||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity)
|
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT(recipe_id, item_id)
|
ON CONFLICT(recipe_id, item_id)
|
||||||
DO UPDATE SET quantity = excluded.quantity
|
DO UPDATE SET quantity = excluded.quantity
|
||||||
`).run(Number(recipeComponents[1]), payload.itemId, payload.quantity)
|
`).run(Number(recipeComponents[1]), payload.itemId, quantity)
|
||||||
writeAdminOverrides(database)
|
writeAdminOverrides(database)
|
||||||
sendJson(response, 200, { ok: true })
|
sendJson(response, 200, { ok: true })
|
||||||
return
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
import { isIP } from 'node:net'
|
import { isIP } from 'node:net'
|
||||||
import { extname, resolve, sep } from 'node:path'
|
import { extname, resolve, sep } from 'node:path'
|
||||||
import { DatabaseSync } from 'node:sqlite'
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
|
import { catalogPayload } from './catalog.mjs'
|
||||||
|
|
||||||
const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url))
|
const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url))
|
||||||
const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url))
|
const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url))
|
||||||
@@ -858,6 +859,8 @@ export function getProfile(database, characterId, accountId) {
|
|||||||
quantity,
|
quantity,
|
||||||
owned,
|
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
|
const { itemId, slug, name, slot, rarity, itemLevel, healingPower, maxResourceBonus, glyph, description, setId, setSlug, setName } = recipe
|
||||||
return {
|
return {
|
||||||
id: recipe.id,
|
id: recipe.id,
|
||||||
@@ -880,7 +883,8 @@ export function getProfile(database, characterId, accountId) {
|
|||||||
setName,
|
setName,
|
||||||
},
|
},
|
||||||
components,
|
components,
|
||||||
canCraft: components.every((component) => component.owned >= component.quantity),
|
canCraft: hasRequiredComponents
|
||||||
|
&& components.every((component) => component.owned >= component.quantity),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
dungeons: dungeons.map((dungeon) => ({
|
dungeons: dungeons.map((dungeon) => ({
|
||||||
@@ -1746,6 +1750,9 @@ function craftItem(database, characterId, recipeId) {
|
|||||||
WHERE crafting_recipe_components.recipe_id = ?
|
WHERE crafting_recipe_components.recipe_id = ?
|
||||||
`).all(characterId, recipeId)
|
`).all(characterId, recipeId)
|
||||||
if (components.length === 0) throw new Error('That recipe has no component requirements.')
|
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)
|
const missing = components.find((component) => component.owned < component.quantity)
|
||||||
if (missing) {
|
if (missing) {
|
||||||
const item = itemById(database, missing.itemId)
|
const item = itemById(database, missing.itemId)
|
||||||
@@ -1845,6 +1852,10 @@ function upgradeItem(database, characterId, itemId) {
|
|||||||
AND character_inventory.character_id = ?
|
AND character_inventory.character_id = ?
|
||||||
WHERE crafting_recipe_components.recipe_id = ?
|
WHERE crafting_recipe_components.recipe_id = ?
|
||||||
`).all(characterId, targetRecipe.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)
|
const missing = components.find((component) => component.owned < component.quantity)
|
||||||
if (missing) {
|
if (missing) {
|
||||||
const componentItem = itemById(database, missing.itemId)
|
const componentItem = itemById(database, missing.itemId)
|
||||||
@@ -2686,6 +2697,11 @@ export async function handleApiRequest(request, response, next) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.url === '/api/catalog' && request.method === 'GET') {
|
||||||
|
sendJson(response, 200, catalogPayload(getProfile(database, 1)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const session = requireSession(database, request)
|
const session = requireSession(database, request)
|
||||||
|
|
||||||
if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
|
if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
|
||||||
|
|||||||
+227
-13
@@ -2664,10 +2664,15 @@ h2 {
|
|||||||
|
|
||||||
.crafting-layout {
|
.crafting-layout {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
grid-template-columns: minmax(134px, 0.42fr) minmax(248px, 1fr) minmax(190px, 0.72fr);
|
grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr);
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crafting-available-panel {
|
||||||
|
gap: 6px;
|
||||||
|
grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr);
|
||||||
|
}
|
||||||
|
|
||||||
.crafting-filters {
|
.crafting-filters {
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
}
|
}
|
||||||
@@ -4154,13 +4159,14 @@ h2 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
grid-template-columns: minmax(210px, 0.55fr) minmax(360px, 1fr) minmax(320px, 0.85fr);
|
grid-template-columns: minmax(230px, 1fr) minmax(0, 2fr);
|
||||||
margin-top: 13px;
|
margin-top: 13px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-filters,
|
.crafting-filters,
|
||||||
|
.crafting-available-panel,
|
||||||
.crafting-list-panel,
|
.crafting-list-panel,
|
||||||
.crafting-detail-panel {
|
.crafting-detail-panel {
|
||||||
background: var(--panel-light);
|
background: var(--panel-light);
|
||||||
@@ -4177,10 +4183,20 @@ h2 {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crafting-available-panel {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: minmax(360px, 1.05fr) minmax(280px, 0.95fr);
|
||||||
|
outline: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.crafting-filter-grid {
|
.crafting-filter-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-filter-grid button,
|
.crafting-filter-grid button,
|
||||||
@@ -6573,7 +6589,8 @@ h2 {
|
|||||||
|
|
||||||
.gear-summary,
|
.gear-summary,
|
||||||
.equipment-layout,
|
.equipment-layout,
|
||||||
.crafting-layout {
|
.crafting-layout,
|
||||||
|
.crafting-available-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7244,9 +7261,203 @@ h2 {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-class-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: minmax(220px, 0.35fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list {
|
||||||
|
background: #1c1e25;
|
||||||
|
border: 2px solid #090a0d;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
outline: 2px solid #494754;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list button {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--panel-light);
|
||||||
|
border: 2px solid #090a0d;
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
grid-template-columns: 38px 1fr;
|
||||||
|
min-height: 54px;
|
||||||
|
outline: 2px solid #41404a;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list button.active,
|
||||||
|
.admin-class-list button:hover {
|
||||||
|
outline-color: var(--class-color, var(--gold));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list button > span,
|
||||||
|
.admin-class-hero > span {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--class-color, var(--gold));
|
||||||
|
color: #111217;
|
||||||
|
display: flex;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 38px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list strong,
|
||||||
|
.admin-class-list small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-list small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-hero {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--panel-light);
|
||||||
|
border: 2px solid #090a0d;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: 54px minmax(180px, auto) minmax(0, 1fr);
|
||||||
|
outline: 2px solid #494754;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-hero > span {
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-hero h2 {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-hero small,
|
||||||
|
.admin-class-hero p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-table {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-table-head,
|
||||||
|
.admin-class-row {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: minmax(230px, 1.5fr) minmax(90px, 0.7fr) minmax(110px, 0.6fr) minmax(65px, 0.45fr) minmax(80px, 0.5fr) minmax(70px, 0.45fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-table-head {
|
||||||
|
color: var(--gold);
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7px;
|
||||||
|
padding: 0 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row {
|
||||||
|
background: var(--panel-light);
|
||||||
|
border: 2px solid #090a0d;
|
||||||
|
outline: 2px solid #494754;
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row > span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row > span:first-child {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: 30px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row i {
|
||||||
|
color: var(--gold);
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row strong,
|
||||||
|
.admin-class-row small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-row small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent {
|
||||||
|
background: var(--panel-light);
|
||||||
|
border: 2px solid #090a0d;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
outline: 2px solid #494754;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent > div {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent > div > span {
|
||||||
|
color: var(--gold);
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent small,
|
||||||
|
.admin-class-talent p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-class-talent em {
|
||||||
|
color: var(--green);
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.admin-upgrade-toolbar,
|
.admin-upgrade-toolbar,
|
||||||
.admin-upgrade-step {
|
.admin-upgrade-step,
|
||||||
|
.admin-class-layout,
|
||||||
|
.admin-class-hero,
|
||||||
|
.admin-class-table-head,
|
||||||
|
.admin-class-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7678,15 +7889,18 @@ h2 {
|
|||||||
|
|
||||||
.workshop-shell .crafting-layout {
|
.workshop-shell .crafting-layout {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr);
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filters {
|
.workshop-shell .crafting-available-panel {
|
||||||
display: grid;
|
gap: 6px;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workshop-shell .crafting-filters {
|
||||||
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filter-grid,
|
.workshop-shell .crafting-filter-grid,
|
||||||
@@ -7695,7 +7909,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filter-grid {
|
.workshop-shell .crafting-filter-grid {
|
||||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filter-grid button {
|
.workshop-shell .crafting-filter-grid button {
|
||||||
@@ -7985,7 +8199,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-layout {
|
.workshop-shell .crafting-layout {
|
||||||
grid-template-columns: 110px minmax(0, 1fr) 174px;
|
grid-template-columns: 110px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filter-grid {
|
.workshop-shell .crafting-filter-grid {
|
||||||
@@ -8189,7 +8403,7 @@ h2 {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-shell .crafting-filters {
|
.workshop-shell .crafting-available-panel {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { Dispatch, SetStateAction } from 'react'
|
import type { CSSProperties, Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
type AdminItem = {
|
type AdminItem = {
|
||||||
id: number
|
id: number
|
||||||
@@ -76,6 +76,49 @@ type AdminUpgradePath = {
|
|||||||
toItemId: number
|
toItemId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminAbility = {
|
||||||
|
id: number
|
||||||
|
classId: number
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
spellType: string
|
||||||
|
cost: number
|
||||||
|
cooldown: number
|
||||||
|
power: number
|
||||||
|
unlockLevel: number
|
||||||
|
glyph: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminTalent = {
|
||||||
|
id: number
|
||||||
|
classId: number
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
maxRank: number
|
||||||
|
tier: number
|
||||||
|
branch: number
|
||||||
|
prerequisiteTalentId: number | null
|
||||||
|
prerequisiteRank: number
|
||||||
|
prerequisiteName: string | null
|
||||||
|
effectType: string
|
||||||
|
effectValuePerRank: number
|
||||||
|
glyph: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminClass = {
|
||||||
|
id: number
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
resourceName: string
|
||||||
|
maxResource: number
|
||||||
|
themeColor: string
|
||||||
|
description: string
|
||||||
|
abilities: AdminAbility[]
|
||||||
|
talents: AdminTalent[]
|
||||||
|
}
|
||||||
|
|
||||||
type AdminData = {
|
type AdminData = {
|
||||||
items: AdminItem[]
|
items: AdminItem[]
|
||||||
encounters: AdminEncounter[]
|
encounters: AdminEncounter[]
|
||||||
@@ -84,9 +127,10 @@ type AdminData = {
|
|||||||
craftingRecipes: AdminRecipe[]
|
craftingRecipes: AdminRecipe[]
|
||||||
dungeons: AdminDungeon[]
|
dungeons: AdminDungeon[]
|
||||||
gearUpgradePaths: AdminUpgradePath[]
|
gearUpgradePaths: AdminUpgradePath[]
|
||||||
|
classes: AdminClass[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades'
|
type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades' | 'classes'
|
||||||
type SavingState = Record<string, boolean>
|
type SavingState = Record<string, boolean>
|
||||||
type SetData = Dispatch<SetStateAction<AdminData | null>>
|
type SetData = Dispatch<SetStateAction<AdminData | null>>
|
||||||
type SetSaving = Dispatch<SetStateAction<SavingState>>
|
type SetSaving = Dispatch<SetStateAction<SavingState>>
|
||||||
@@ -99,6 +143,7 @@ const tabs: { id: AdminTab; label: string }[] = [
|
|||||||
{ id: 'loot', label: 'Loot' },
|
{ id: 'loot', label: 'Loot' },
|
||||||
{ id: 'crafting', label: 'Crafting' },
|
{ id: 'crafting', label: 'Crafting' },
|
||||||
{ id: 'upgrades', label: 'Upgrades' },
|
{ id: 'upgrades', label: 'Upgrades' },
|
||||||
|
{ id: 'classes', label: 'Classes' },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
|
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
@@ -143,6 +188,7 @@ export function AdminScreen({ onBack }: { onBack: () => void }) {
|
|||||||
{tab === 'loot' && <LootTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
{tab === 'loot' && <LootTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||||
{tab === 'crafting' && <CraftingTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
{tab === 'crafting' && <CraftingTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||||
{tab === 'upgrades' && <UpgradesTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
{tab === 'upgrades' && <UpgradesTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
|
||||||
|
{tab === 'classes' && <ClassesTab data={data} />}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -830,7 +876,9 @@ function CraftingTab({ data, setData, setSaving, saving }: {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<h3 className="admin-loot-title">Required Components</h3>
|
<h3 className="admin-loot-title">Required Components</h3>
|
||||||
{(!recipe || recipe.components.length === 0) && <p className="admin-empty">No component requirements.</p>}
|
{(!recipe || recipe.components.length === 0) && (
|
||||||
|
<p className="admin-empty">No component requirements. Crafting and upgrades are blocked until materials are added.</p>
|
||||||
|
)}
|
||||||
<div className="admin-loot-list">
|
<div className="admin-loot-list">
|
||||||
{recipe?.components.map((comp) => (
|
{recipe?.components.map((comp) => (
|
||||||
<div key={comp.itemId} className="admin-loot-row">
|
<div key={comp.itemId} className="admin-loot-row">
|
||||||
@@ -981,7 +1029,7 @@ function UpgradesTab({ data, setData, setSaving, saving }: {
|
|||||||
{target
|
{target
|
||||||
? `Target requirements: ${targetRecipe && targetRecipe.components.length > 0
|
? `Target requirements: ${targetRecipe && targetRecipe.components.length > 0
|
||||||
? targetRecipe.components.map((component) => `${component.quantity}x ${itemName(data, component.itemId)}`).join(', ')
|
? targetRecipe.components.map((component) => `${component.quantity}x ${itemName(data, component.itemId)}`).join(', ')
|
||||||
: 'none'}`
|
: 'none configured - upgrade blocked until materials are added'}`
|
||||||
: 'No next upgrade selected.'}
|
: 'No next upgrade selected.'}
|
||||||
</p>
|
</p>
|
||||||
<div className="admin-edit-actions">
|
<div className="admin-edit-actions">
|
||||||
@@ -1015,6 +1063,98 @@ function UpgradesTab({ data, setData, setSaving, saving }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ClassesTab({ data }: { data: AdminData }) {
|
||||||
|
const [classId, setClassId] = useState(data.classes[0]?.id ?? 0)
|
||||||
|
const selectedClass = data.classes.find((candidate) => candidate.id === classId)
|
||||||
|
?? data.classes[0]
|
||||||
|
?? null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-panel">
|
||||||
|
<div className="admin-class-layout">
|
||||||
|
<aside className="admin-class-list">
|
||||||
|
<p className="eyebrow">Classes</p>
|
||||||
|
{data.classes.map((gameClass) => (
|
||||||
|
<button
|
||||||
|
className={selectedClass?.id === gameClass.id ? 'active' : ''}
|
||||||
|
key={gameClass.id}
|
||||||
|
onClick={() => setClassId(gameClass.id)}
|
||||||
|
style={{ '--class-color': gameClass.themeColor } as CSSProperties}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>{gameClass.name[0]}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{gameClass.name}</strong>
|
||||||
|
<small>{gameClass.resourceName} {gameClass.maxResource}</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{selectedClass ? (
|
||||||
|
<section className="admin-class-detail">
|
||||||
|
<div className="admin-class-hero" style={{ '--class-color': selectedClass.themeColor } as CSSProperties}>
|
||||||
|
<span>{selectedClass.name[0]}</span>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">{selectedClass.slug}</p>
|
||||||
|
<h2>{selectedClass.name}</h2>
|
||||||
|
<small>{selectedClass.resourceName} pool: {selectedClass.maxResource}</small>
|
||||||
|
</div>
|
||||||
|
<p>{selectedClass.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="admin-loot-title">Abilities ({selectedClass.abilities.length})</h3>
|
||||||
|
<div className="admin-class-table">
|
||||||
|
<div className="admin-class-table-head">
|
||||||
|
<span>Ability</span>
|
||||||
|
<span>Type</span>
|
||||||
|
<span>Default Strength</span>
|
||||||
|
<span>Cost</span>
|
||||||
|
<span>Cooldown</span>
|
||||||
|
<span>Unlock</span>
|
||||||
|
</div>
|
||||||
|
{selectedClass.abilities.map((ability) => (
|
||||||
|
<div key={ability.id} className="admin-class-row">
|
||||||
|
<span><i>{ability.glyph}</i><strong>{ability.name}</strong><small>{ability.description}</small></span>
|
||||||
|
<span>{ability.spellType}</span>
|
||||||
|
<span>{ability.power}</span>
|
||||||
|
<span>{ability.cost}</span>
|
||||||
|
<span>{ability.cooldown}s</span>
|
||||||
|
<span>Lvl {ability.unlockLevel}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="admin-loot-title">Talents ({selectedClass.talents.length})</h3>
|
||||||
|
<div className="admin-class-talent-grid">
|
||||||
|
{selectedClass.talents.map((talent) => (
|
||||||
|
<article key={talent.id} className="admin-class-talent">
|
||||||
|
<div>
|
||||||
|
<span>{talent.glyph}</span>
|
||||||
|
<strong>{talent.name}</strong>
|
||||||
|
</div>
|
||||||
|
<small>Tier {talent.tier} · Branch {talent.branch} · Max {talent.maxRank}</small>
|
||||||
|
<p>{talent.description}</p>
|
||||||
|
<em>{talent.effectType}: {talent.effectValuePerRank}/rank</em>
|
||||||
|
{talent.prerequisiteName && (
|
||||||
|
<small>Requires {talent.prerequisiteName} rank {talent.prerequisiteRank}</small>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<p className="admin-empty">No classes found.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function jsonRequest(method: 'POST' | 'PUT', body: unknown): RequestInit {
|
function jsonRequest(method: 'POST' | 'PUT', body: unknown): RequestInit {
|
||||||
return {
|
return {
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -596,8 +596,12 @@ export function EquipmentScreen({
|
|||||||
/>
|
/>
|
||||||
<div className="crafting-layout">
|
<div className="crafting-layout">
|
||||||
<aside className="crafting-filters">
|
<aside className="crafting-filters">
|
||||||
|
<EquipmentHeading
|
||||||
|
eyebrow="Slots"
|
||||||
|
title="Gear Slots"
|
||||||
|
detail={slotFilter === 'all' ? 'All' : SLOT_LABELS[slotFilter]}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Slot</p>
|
|
||||||
<div className="crafting-filter-grid">
|
<div className="crafting-filter-grid">
|
||||||
<button
|
<button
|
||||||
className={slotFilter === 'all' ? 'active' : ''}
|
className={slotFilter === 'all' ? 'active' : ''}
|
||||||
@@ -657,10 +661,11 @@ export function EquipmentScreen({
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<section className="crafting-available-panel">
|
||||||
<section className="crafting-list-panel">
|
<section className="crafting-list-panel">
|
||||||
<EquipmentHeading
|
<EquipmentHeading
|
||||||
eyebrow="Recipes"
|
eyebrow="Available Gear"
|
||||||
title={slotFilter === 'all' ? 'Available' : SLOT_LABELS[slotFilter]}
|
title={slotFilter === 'all' ? 'Craftable Gear' : SLOT_LABELS[slotFilter]}
|
||||||
detail={`Page ${recipePage + 1}/${recipePageCount}`}
|
detail={`Page ${recipePage + 1}/${recipePageCount}`}
|
||||||
/>
|
/>
|
||||||
{filteredRecipes.length === 0 ? (
|
{filteredRecipes.length === 0 ? (
|
||||||
@@ -719,6 +724,9 @@ export function EquipmentScreen({
|
|||||||
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
|
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="crafting-components">
|
<div className="crafting-components">
|
||||||
|
{selectedRecipe.components.length === 0 && (
|
||||||
|
<p className="inventory-empty">No materials configured. Crafting disabled.</p>
|
||||||
|
)}
|
||||||
{selectedRecipe.components.map((component) => (
|
{selectedRecipe.components.map((component) => (
|
||||||
<div
|
<div
|
||||||
className={component.owned >= component.quantity ? 'ready' : 'missing'}
|
className={component.owned >= component.quantity ? 'ready' : 'missing'}
|
||||||
@@ -735,6 +743,7 @@ export function EquipmentScreen({
|
|||||||
<p className="inventory-empty">Select a recipe.</p>
|
<p className="inventory-empty">Select a recipe.</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+74
-4
@@ -1,4 +1,5 @@
|
|||||||
import starterProfile from './offline-starter-profile.json'
|
import starterProfile from './offline-starter-profile.json'
|
||||||
|
import { bundledCatalogHash } from './offline-catalog-meta'
|
||||||
import type {
|
import type {
|
||||||
Account,
|
Account,
|
||||||
AuthSession,
|
AuthSession,
|
||||||
@@ -82,6 +83,12 @@ type OnlineCache = {
|
|||||||
dirty: boolean
|
dirty: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CatalogCache = {
|
||||||
|
version: 1
|
||||||
|
hash: string
|
||||||
|
profile: CharacterProfile
|
||||||
|
}
|
||||||
|
|
||||||
export type CloudSyncStatus = {
|
export type CloudSyncStatus = {
|
||||||
available: boolean
|
available: boolean
|
||||||
dirty: boolean
|
dirty: boolean
|
||||||
@@ -102,6 +109,8 @@ type LocalSaveStore = {
|
|||||||
const modeKey = 'chronicle.repositoryMode'
|
const modeKey = 'chronicle.repositoryMode'
|
||||||
const offlineSaveKey = 'chronicle.offlineSave.v1'
|
const offlineSaveKey = 'chronicle.offlineSave.v1'
|
||||||
const onlineCacheKey = 'chronicle.onlineCache.v1'
|
const onlineCacheKey = 'chronicle.onlineCache.v1'
|
||||||
|
const catalogCacheKey = 'chronicle.catalog.v1'
|
||||||
|
const catalogBundleKey = 'chronicle.catalog.bundleHash.v1'
|
||||||
const authTokenKey = 'chronicle.authToken.v1'
|
const authTokenKey = 'chronicle.authToken.v1'
|
||||||
const offlineAccount = { id: -1, username: 'Offline' }
|
const offlineAccount = { id: -1, username: 'Offline' }
|
||||||
const ABILITY_SLOT_COUNT = 6
|
const ABILITY_SLOT_COUNT = 6
|
||||||
@@ -281,8 +290,42 @@ function clearOnlineCache() {
|
|||||||
localStorage.removeItem(onlineCacheKey)
|
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 {
|
function buildProfile(save: OfflineSave): CharacterProfile {
|
||||||
const static_ = clone(starterProfile) as CharacterProfile
|
const static_ = clone(activeCatalog().profile)
|
||||||
const cd = save.characters[save.activeClassId]
|
const cd = save.characters[save.activeClassId]
|
||||||
const gameClass = static_.classes.find((c) => c.id === save.activeClassId)!
|
const gameClass = static_.classes.find((c) => c.id === save.activeClassId)!
|
||||||
|
|
||||||
@@ -364,10 +407,13 @@ function updateCraftingRecipes(profile: CharacterProfile) {
|
|||||||
...component,
|
...component,
|
||||||
owned: owned.get(component.item.id) ?? 0,
|
owned: owned.get(component.item.id) ?? 0,
|
||||||
}))
|
}))
|
||||||
|
const hasRequiredComponents = components.length > 0
|
||||||
|
&& components.every((component) => component.quantity > 0)
|
||||||
return {
|
return {
|
||||||
...recipe,
|
...recipe,
|
||||||
components,
|
components,
|
||||||
canCraft: components.every((component) => component.owned >= component.quantity),
|
canCraft: hasRequiredComponents
|
||||||
|
&& components.every((component) => component.owned >= component.quantity),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -654,6 +700,23 @@ function isNetworkError(reason: unknown): reason is NetworkError {
|
|||||||
return reason instanceof Error && Boolean((reason as NetworkError).network)
|
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 {
|
function cachedOnlineSession(): AuthSession | null {
|
||||||
const cache = readOnlineCache()
|
const cache = readOnlineCache()
|
||||||
if (!cache) return null
|
if (!cache) return null
|
||||||
@@ -695,6 +758,7 @@ async function pushServerSyncSave(save: OfflineSave): Promise<{ profile: Charact
|
|||||||
async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
|
async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
|
||||||
const cache = readOnlineCache()
|
const cache = readOnlineCache()
|
||||||
if (session.token) writeAuthToken(session.token)
|
if (session.token) writeAuthToken(session.token)
|
||||||
|
await refreshCatalogFromServer()
|
||||||
if (!session.account || !session.profile) {
|
if (!session.account || !session.profile) {
|
||||||
if (session.account && cache?.account.id === session.account.id) {
|
if (session.account && cache?.account.id === session.account.id) {
|
||||||
return {
|
return {
|
||||||
@@ -845,7 +909,7 @@ const serverRepository: GameRepository = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyCharacterData(classId: number): CharacterData {
|
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 gc = static_.classes.find((c) => c.id === classId)!
|
||||||
const talentRanks: Record<string, number> = {}
|
const talentRanks: Record<string, number> = {}
|
||||||
for (const t of gc.talents) talentRanks[String(t.id)] = 0
|
for (const t of gc.talents) talentRanks[String(t.id)] = 0
|
||||||
@@ -1301,12 +1365,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
|||||||
if (!recipe) throw new Error('That crafting recipe does not exist.')
|
if (!recipe) throw new Error('That crafting recipe does not exist.')
|
||||||
const requiresUpgrade = !DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel)
|
const requiresUpgrade = !DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel)
|
||||||
if (requiresUpgrade) throw new Error('Upgrade the previous item tier instead.')
|
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)
|
const missing = recipe.components.find((component) => component.owned < component.quantity)
|
||||||
if (missing) {
|
if (missing) {
|
||||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`)
|
throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const component of recipe.components) {
|
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)
|
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.`)
|
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to craft this item.`)
|
||||||
owned.quantity -= component.quantity
|
owned.quantity -= component.quantity
|
||||||
@@ -1331,12 +1397,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
|||||||
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, item)
|
? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, item)
|
||||||
: null
|
: null
|
||||||
if (!targetRecipe) throw new Error('No upgrade is available for this item.')
|
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)
|
const missing = targetRecipe.components.find((component) => component.owned < component.quantity)
|
||||||
if (missing) {
|
if (missing) {
|
||||||
throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`)
|
throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const component of targetRecipe.components) {
|
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)
|
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.`)
|
if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to upgrade this item.`)
|
||||||
owned.quantity -= component.quantity
|
owned.quantity -= component.quantity
|
||||||
@@ -1537,7 +1605,9 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
|||||||
if (!cache) {
|
if (!cache) {
|
||||||
throw new Error('No signed-in save is available for cloud sync.')
|
throw new Error('No signed-in save is available for cloud sync.')
|
||||||
}
|
}
|
||||||
|
await refreshCatalogFromServer()
|
||||||
const synced = await pushServerSyncSave(cache.save)
|
const synced = await pushServerSyncSave(cache.save)
|
||||||
|
await refreshCatalogFromServer()
|
||||||
writeOnlineCache({
|
writeOnlineCache({
|
||||||
version: 1,
|
version: 1,
|
||||||
account: cache.account,
|
account: cache.account,
|
||||||
@@ -1545,7 +1615,7 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
|||||||
dirty: false,
|
dirty: false,
|
||||||
})
|
})
|
||||||
writeMode('online')
|
writeMode('online')
|
||||||
return synced.profile
|
return buildProfile(synced.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectOnlineMode() {
|
export function selectOnlineMode() {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const bundledCatalogHash = '07506c52bab428c439f09a9b82e39e6eff2b243fb972d664da48852059bcd937'
|
||||||
@@ -1437,6 +1437,22 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
|
{
|
||||||
|
"item": {
|
||||||
|
"id": 383002,
|
||||||
|
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
|
||||||
|
"name": "Green Bulldrome Coin",
|
||||||
|
"slot": "component",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"itemLevel": 10,
|
||||||
|
"healingPower": 0,
|
||||||
|
"maxResourceBonus": 0,
|
||||||
|
"glyph": "$",
|
||||||
|
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
||||||
|
},
|
||||||
|
"quantity": 5,
|
||||||
|
"owned": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"item": {
|
"item": {
|
||||||
"id": 683002,
|
"id": 683002,
|
||||||
@@ -1450,7 +1466,7 @@
|
|||||||
"glyph": "$",
|
"glyph": "$",
|
||||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||||
},
|
},
|
||||||
"quantity": 10,
|
"quantity": 5,
|
||||||
"owned": 0
|
"owned": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1477,6 +1493,22 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
|
{
|
||||||
|
"item": {
|
||||||
|
"id": 383002,
|
||||||
|
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
|
||||||
|
"name": "Green Bulldrome Coin",
|
||||||
|
"slot": "component",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"itemLevel": 10,
|
||||||
|
"healingPower": 0,
|
||||||
|
"maxResourceBonus": 0,
|
||||||
|
"glyph": "$",
|
||||||
|
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
||||||
|
},
|
||||||
|
"quantity": 5,
|
||||||
|
"owned": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"item": {
|
"item": {
|
||||||
"id": 683002,
|
"id": 683002,
|
||||||
@@ -1490,7 +1522,7 @@
|
|||||||
"glyph": "$",
|
"glyph": "$",
|
||||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||||
},
|
},
|
||||||
"quantity": 10,
|
"quantity": 5,
|
||||||
"owned": 0
|
"owned": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1517,6 +1549,22 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
|
{
|
||||||
|
"item": {
|
||||||
|
"id": 383002,
|
||||||
|
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
|
||||||
|
"name": "Green Bulldrome Coin",
|
||||||
|
"slot": "component",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"itemLevel": 10,
|
||||||
|
"healingPower": 0,
|
||||||
|
"maxResourceBonus": 0,
|
||||||
|
"glyph": "$",
|
||||||
|
"description": "A boss coin from Bulldrome used for item level 10 crafting."
|
||||||
|
},
|
||||||
|
"quantity": 5,
|
||||||
|
"owned": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"item": {
|
"item": {
|
||||||
"id": 683002,
|
"id": 683002,
|
||||||
@@ -1530,7 +1578,7 @@
|
|||||||
"glyph": "$",
|
"glyph": "$",
|
||||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||||
},
|
},
|
||||||
"quantity": 10,
|
"quantity": 5,
|
||||||
"owned": 0
|
"owned": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1797,7 +1845,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1203,
|
"id": 1203,
|
||||||
@@ -1820,7 +1868,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1201,
|
"id": 1201,
|
||||||
@@ -1843,7 +1891,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1204,
|
"id": 1204,
|
||||||
@@ -1866,7 +1914,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1205,
|
"id": 1205,
|
||||||
@@ -1889,7 +1937,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1206,
|
"id": 1206,
|
||||||
@@ -1912,7 +1960,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1209,
|
"id": 1209,
|
||||||
@@ -1935,7 +1983,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1208,
|
"id": 1208,
|
||||||
@@ -1958,7 +2006,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1207,
|
"id": 1207,
|
||||||
@@ -1981,7 +2029,7 @@
|
|||||||
"setName": null
|
"setName": null
|
||||||
},
|
},
|
||||||
"components": [],
|
"components": [],
|
||||||
"canCraft": true
|
"canCraft": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1302,
|
"id": 1302,
|
||||||
|
|||||||
Reference in New Issue
Block a user