Compare commits

..

3 Commits

Author SHA1 Message Date
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
12 changed files with 600 additions and 143 deletions
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 66
versionName "1.0.43" versionName "1.0.46"
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,