Compare commits

..

4 Commits

Author SHA1 Message Date
Warren H 05bd70a9fe Android build v1.0.47 2026-06-20 23:49:20 -04:00
Warren H bb5c7e6e21 Android build v1.0.46 2026-06-20 23:45:21 -04:00
Warren H 14bec979e6 Android build v1.0.45 2026-06-20 23:13:55 -04:00
Warren H 4b45483ac3 Android build v1.0.44 2026-06-20 23:04:39 -04:00
16 changed files with 600 additions and 143 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -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 67
versionName "1.0.43" versionName "1.0.47"
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
View File
@@ -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);
+33 -1
View File
@@ -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
+11 -1
View File
@@ -858,6 +858,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 +882,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 +1749,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 +1851,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)
+227 -13
View File
@@ -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);
} }
+144 -4
View File
@@ -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,
+77 -68
View File
@@ -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,38 +661,39 @@ export function EquipmentScreen({
</div> </div>
</aside> </aside>
<section className="crafting-list-panel"> <section className="crafting-available-panel">
<EquipmentHeading <section className="crafting-list-panel">
eyebrow="Recipes" <EquipmentHeading
title={slotFilter === 'all' ? 'Available' : SLOT_LABELS[slotFilter]} eyebrow="Available Gear"
detail={`Page ${recipePage + 1}/${recipePageCount}`} 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> {filteredRecipes.length === 0 ? (
) : ( <p className="inventory-empty">No recipes match filters.</p>
<div className="crafting-list"> ) : (
{recipePageItems.map((recipe) => ( <div className="crafting-list">
<button {recipePageItems.map((recipe) => (
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`} <button
key={recipe.id} className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
onClick={() => setSelectedRecipeId(recipe.id)} key={recipe.id}
type="button" onClick={() => setSelectedRecipeId(recipe.id)}
> type="button"
<span>{recipe.item.glyph}</span> >
<div> <span>{recipe.item.glyph}</span>
<strong>{recipe.item.name}</strong> <div>
<small> <strong>{recipe.item.name}</strong>
{SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel} <small>
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''} {SLOT_LABELS[recipe.item.slot]} - Item Level {recipe.item.itemLevel}
</small> {recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
</div> </small>
<i className={recipe.canCraft ? 'ready' : 'missing'}> </div>
{recipe.canCraft ? 'Ready' : 'Needs materials'} <i className={recipe.canCraft ? 'ready' : 'missing'}>
</i> {recipe.canCraft ? 'Ready' : 'Needs materials'}
</button> </i>
))} </button>
</div> ))}
)} </div>
)}
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && ( {filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
<ListPager <ListPager
label={`Page ${recipePage + 1} / ${recipePageCount}`} label={`Page ${recipePage + 1} / ${recipePageCount}`}
@@ -698,42 +703,46 @@ export function EquipmentScreen({
previousDisabled={recipePage <= 0} previousDisabled={recipePage <= 0}
/> />
)} )}
<div className="crafting-action-row"> <div className="crafting-action-row">
<button <button
className="primary-button" className="primary-button"
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting} disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
onClick={craftSelected} onClick={craftSelected}
type="button" type="button"
> >
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'} {crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button> </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> </div>
) : ( </section>
<p className="inventory-empty">Select a recipe.</p>
)} <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> </section>
</div> </div>
</section> </section>
+8 -1
View File
@@ -364,10 +364,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),
} }
}) })
} }
@@ -1301,12 +1304,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 +1336,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
+9 -9
View File
@@ -1797,7 +1797,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1203, "id": 1203,
@@ -1820,7 +1820,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1201, "id": 1201,
@@ -1843,7 +1843,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1204, "id": 1204,
@@ -1866,7 +1866,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1205, "id": 1205,
@@ -1889,7 +1889,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1206, "id": 1206,
@@ -1912,7 +1912,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1209, "id": 1209,
@@ -1935,7 +1935,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1208, "id": 1208,
@@ -1958,7 +1958,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1207, "id": 1207,
@@ -1981,7 +1981,7 @@
"setName": null "setName": null
}, },
"components": [], "components": [],
"canCraft": true "canCraft": false
}, },
{ {
"id": 1302, "id": 1302,