Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e24aecad8 | |||
| c9fb28ab6d | |||
| 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.
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.warren.iwanttoheal"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 63
|
||||
versionName "1.0.43"
|
||||
versionCode 71
|
||||
versionName "1.0.52"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// 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 (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 (2103, 2286101, 101, 100, 1);
|
||||
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) VALUES (2303, 783103, 103, 100, 1);
|
||||
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) VALUES (2503, 789103, 103, 100, 1);
|
||||
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) VALUES (2703, 986104, 104, 100, 1);
|
||||
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) VALUES (2903, 1183105, 105, 100, 1);
|
||||
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) VALUES (3103, 1189105, 105, 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;
|
||||
@@ -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 (1008, 583001, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1009, 583001, 5);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1101, 683002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1102, 683002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1103, 683002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1104, 783002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1105, 783002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1106, 783002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 10);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 1283004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 1283004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 1283004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1304, 1383004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1305, 1383004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1306, 1383004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1307, 1483004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1308, 1483004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1309, 1483004, 20);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1401, 1583005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1402, 1583005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1403, 1583005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1404, 1683005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1405, 1683005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1406, 1683005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1407, 1783005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1408, 1783005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1409, 1783005, 25);
|
||||
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (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) VALUES (2004, 2286101, 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) VALUES (2006, 2286101, 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) VALUES (2008, 2289101, 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 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);
|
||||
|
||||
+15
@@ -1908,6 +1908,21 @@ JOIN coin_sources
|
||||
ON coin_sources.encounter_id = crafting_recipes.source_encounter_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;
|
||||
|
||||
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+33
-1
@@ -375,6 +375,31 @@ const server = createServer(async (request, response) => {
|
||||
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,
|
||||
@@ -383,6 +408,11 @@ const server = createServer(async (request, response) => {
|
||||
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
|
||||
}
|
||||
@@ -499,12 +529,14 @@ 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
|
||||
|
||||
@@ -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 { 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))
|
||||
@@ -858,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,
|
||||
@@ -880,7 +883,8 @@ 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) => ({
|
||||
@@ -1746,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)
|
||||
@@ -1845,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)
|
||||
@@ -2686,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') {
|
||||
|
||||
+230
-13
@@ -621,6 +621,7 @@ textarea:focus-visible,
|
||||
min-height: 120px;
|
||||
outline: 2px solid #3a3944;
|
||||
padding: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dual-top-member.selected {
|
||||
@@ -2664,10 +2665,15 @@ h2 {
|
||||
|
||||
.crafting-layout {
|
||||
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;
|
||||
}
|
||||
|
||||
.crafting-available-panel {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr);
|
||||
}
|
||||
|
||||
.crafting-filters {
|
||||
gap: 7px;
|
||||
}
|
||||
@@ -4154,13 +4160,14 @@ h2 {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
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;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.crafting-filters,
|
||||
.crafting-available-panel,
|
||||
.crafting-list-panel,
|
||||
.crafting-detail-panel {
|
||||
background: var(--panel-light);
|
||||
@@ -4177,10 +4184,20 @@ h2 {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.crafting-filter-grid button,
|
||||
@@ -5238,6 +5255,7 @@ h2 {
|
||||
|
||||
.member-health .health-text {
|
||||
color: var(--ink);
|
||||
display: none;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
font-style: normal;
|
||||
@@ -5286,6 +5304,7 @@ h2 {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.floating-heal {
|
||||
@@ -6573,7 +6592,8 @@ h2 {
|
||||
|
||||
.gear-summary,
|
||||
.equipment-layout,
|
||||
.crafting-layout {
|
||||
.crafting-layout,
|
||||
.crafting-available-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -7244,9 +7264,203 @@ h2 {
|
||||
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) {
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -7678,15 +7892,18 @@ h2 {
|
||||
|
||||
.workshop-shell .crafting-layout {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
display: grid;
|
||||
.workshop-shell .crafting-available-panel {
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid,
|
||||
@@ -7695,7 +7912,7 @@ h2 {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -7985,7 +8202,7 @@ h2 {
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-layout {
|
||||
grid-template-columns: 110px minmax(0, 1fr) 174px;
|
||||
grid-template-columns: 110px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filter-grid {
|
||||
@@ -8189,7 +8406,7 @@ h2 {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workshop-shell .crafting-filters {
|
||||
.workshop-shell .crafting-available-panel {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { CSSProperties, Dispatch, SetStateAction } from 'react'
|
||||
|
||||
type AdminItem = {
|
||||
id: number
|
||||
@@ -76,6 +76,49 @@ type AdminUpgradePath = {
|
||||
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 = {
|
||||
items: AdminItem[]
|
||||
encounters: AdminEncounter[]
|
||||
@@ -84,9 +127,10 @@ type AdminData = {
|
||||
craftingRecipes: AdminRecipe[]
|
||||
dungeons: AdminDungeon[]
|
||||
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 SetData = Dispatch<SetStateAction<AdminData | null>>
|
||||
type SetSaving = Dispatch<SetStateAction<SavingState>>
|
||||
@@ -99,6 +143,7 @@ const tabs: { id: AdminTab; label: string }[] = [
|
||||
{ id: 'loot', label: 'Loot' },
|
||||
{ id: 'crafting', label: 'Crafting' },
|
||||
{ id: 'upgrades', label: 'Upgrades' },
|
||||
{ id: 'classes', label: 'Classes' },
|
||||
]
|
||||
|
||||
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 === 'crafting' && <CraftingTab 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>
|
||||
)
|
||||
}
|
||||
@@ -830,7 +876,9 @@ function CraftingTab({ data, setData, setSaving, saving }: {
|
||||
)}
|
||||
|
||||
<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">
|
||||
{recipe?.components.map((comp) => (
|
||||
<div key={comp.itemId} className="admin-loot-row">
|
||||
@@ -981,7 +1029,7 @@ function UpgradesTab({ data, setData, setSaving, saving }: {
|
||||
{target
|
||||
? `Target requirements: ${targetRecipe && targetRecipe.components.length > 0
|
||||
? 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.'}
|
||||
</p>
|
||||
<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 {
|
||||
return {
|
||||
method,
|
||||
|
||||
@@ -1375,6 +1375,7 @@ export function CombatScreen({
|
||||
encounterIndex,
|
||||
encounterCount: encounters.length,
|
||||
party,
|
||||
floatingTexts,
|
||||
partySize: dungeon.partySize,
|
||||
selectedId,
|
||||
log,
|
||||
@@ -1430,6 +1431,7 @@ export function CombatScreen({
|
||||
selectedId,
|
||||
spells,
|
||||
freeCastReady,
|
||||
floatingTexts,
|
||||
roguelikeUpgrades,
|
||||
speedMultiplier,
|
||||
status,
|
||||
|
||||
@@ -596,8 +596,12 @@ export function EquipmentScreen({
|
||||
/>
|
||||
<div className="crafting-layout">
|
||||
<aside className="crafting-filters">
|
||||
<EquipmentHeading
|
||||
eyebrow="Slots"
|
||||
title="Gear Slots"
|
||||
detail={slotFilter === 'all' ? 'All' : SLOT_LABELS[slotFilter]}
|
||||
/>
|
||||
<div>
|
||||
<p className="eyebrow">Slot</p>
|
||||
<div className="crafting-filter-grid">
|
||||
<button
|
||||
className={slotFilter === 'all' ? 'active' : ''}
|
||||
@@ -657,38 +661,39 @@ export function EquipmentScreen({
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="crafting-list-panel">
|
||||
<EquipmentHeading
|
||||
eyebrow="Recipes"
|
||||
title={slotFilter === 'all' ? 'Available' : 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>
|
||||
)}
|
||||
<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}`}
|
||||
@@ -698,42 +703,46 @@ export function EquipmentScreen({
|
||||
previousDisabled={recipePage <= 0}
|
||||
/>
|
||||
)}
|
||||
<div className="crafting-action-row">
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
|
||||
onClick={craftSelected}
|
||||
type="button"
|
||||
>
|
||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
||||
</button>
|
||||
</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.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={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
|
||||
onClick={craftSelected}
|
||||
type="button"
|
||||
>
|
||||
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="inventory-empty">Select a recipe.</p>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -1212,6 +1212,9 @@ export function PvPRoguelikeScreen({
|
||||
encounterIndex,
|
||||
encounterCount: encounters.length,
|
||||
party: playerSide.party,
|
||||
floatingTexts: floatingTexts
|
||||
.filter((entry) => entry.side === 'player')
|
||||
.map(({ id, memberId, value }) => ({ id, memberId, value })),
|
||||
partySize: playerSide.party.length,
|
||||
selectedId,
|
||||
log,
|
||||
@@ -1243,6 +1246,7 @@ export function PvPRoguelikeScreen({
|
||||
encounter.maxHealth,
|
||||
encounterIndex,
|
||||
encounters.length,
|
||||
floatingTexts,
|
||||
gameClass.resourceName,
|
||||
lastDevice,
|
||||
log,
|
||||
|
||||
@@ -39,6 +39,11 @@ export type DualScreenCombatState = {
|
||||
encounterIndex: number
|
||||
encounterCount: number
|
||||
party: PartyMember[]
|
||||
floatingTexts: Array<{
|
||||
id: number
|
||||
memberId: string
|
||||
value: number
|
||||
}>
|
||||
partySize: number
|
||||
selectedId: string
|
||||
log: CombatLogEntry[]
|
||||
@@ -599,6 +604,11 @@ export function DualScreenTopCombat({
|
||||
)}
|
||||
<em className="health-text">{Math.ceil(member.health)} / {member.maxHealth}</em>
|
||||
</div>
|
||||
<div className="floating-combat-texts" aria-hidden="true">
|
||||
{state.floatingTexts
|
||||
.filter((entry) => entry.memberId === member.id)
|
||||
.map((entry) => <span className="floating-heal" key={entry.id}>+{entry.value}</span>)}
|
||||
</div>
|
||||
{state.directPartyTargeting && targetBinding && (
|
||||
<div className="member-target-key">
|
||||
<ControllerBindingLabel
|
||||
|
||||
+74
-4
@@ -1,4 +1,5 @@
|
||||
import starterProfile from './offline-starter-profile.json'
|
||||
import { bundledCatalogHash } from './offline-catalog-meta'
|
||||
import type {
|
||||
Account,
|
||||
AuthSession,
|
||||
@@ -82,6 +83,12 @@ type OnlineCache = {
|
||||
dirty: boolean
|
||||
}
|
||||
|
||||
type CatalogCache = {
|
||||
version: 1
|
||||
hash: string
|
||||
profile: CharacterProfile
|
||||
}
|
||||
|
||||
export type CloudSyncStatus = {
|
||||
available: boolean
|
||||
dirty: boolean
|
||||
@@ -102,6 +109,8 @@ 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
|
||||
@@ -281,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)!
|
||||
|
||||
@@ -364,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),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -654,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
|
||||
@@ -695,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 {
|
||||
@@ -845,7 +909,7 @@ 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
|
||||
@@ -1301,12 +1365,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
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
|
||||
@@ -1331,12 +1397,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
? 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
|
||||
@@ -1537,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,
|
||||
@@ -1545,7 +1615,7 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
||||
dirty: false,
|
||||
})
|
||||
writeMode('online')
|
||||
return synced.profile
|
||||
return buildProfile(synced.save)
|
||||
}
|
||||
|
||||
export function selectOnlineMode() {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const bundledCatalogHash = '07506c52bab428c439f09a9b82e39e6eff2b243fb972d664da48852059bcd937'
|
||||
@@ -1437,6 +1437,22 @@
|
||||
"setName": null
|
||||
},
|
||||
"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": {
|
||||
"id": 683002,
|
||||
@@ -1450,7 +1466,7 @@
|
||||
"glyph": "$",
|
||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||
},
|
||||
"quantity": 10,
|
||||
"quantity": 5,
|
||||
"owned": 0
|
||||
}
|
||||
],
|
||||
@@ -1477,6 +1493,22 @@
|
||||
"setName": null
|
||||
},
|
||||
"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": {
|
||||
"id": 683002,
|
||||
@@ -1490,7 +1522,7 @@
|
||||
"glyph": "$",
|
||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||
},
|
||||
"quantity": 10,
|
||||
"quantity": 5,
|
||||
"owned": 0
|
||||
}
|
||||
],
|
||||
@@ -1517,6 +1549,22 @@
|
||||
"setName": null
|
||||
},
|
||||
"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": {
|
||||
"id": 683002,
|
||||
@@ -1530,7 +1578,7 @@
|
||||
"glyph": "$",
|
||||
"description": "A boss coin from Tigrex used for item level 10 crafting."
|
||||
},
|
||||
"quantity": 10,
|
||||
"quantity": 5,
|
||||
"owned": 0
|
||||
}
|
||||
],
|
||||
@@ -1797,7 +1845,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1203,
|
||||
@@ -1820,7 +1868,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1201,
|
||||
@@ -1843,7 +1891,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1204,
|
||||
@@ -1866,7 +1914,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1205,
|
||||
@@ -1889,7 +1937,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1206,
|
||||
@@ -1912,7 +1960,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1209,
|
||||
@@ -1935,7 +1983,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1208,
|
||||
@@ -1958,7 +2006,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1207,
|
||||
@@ -1981,7 +2029,7 @@
|
||||
"setName": null
|
||||
},
|
||||
"components": [],
|
||||
"canCraft": true
|
||||
"canCraft": false
|
||||
},
|
||||
{
|
||||
"id": 1302,
|
||||
|
||||
Reference in New Issue
Block a user