diff --git a/IWantToHeal-Thor-v1.0.44.apk b/IWantToHeal-Thor-v1.0.44.apk new file mode 100644 index 0000000..75e750d Binary files /dev/null and b/IWantToHeal-Thor-v1.0.44.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 6bdc452..4814880 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 63 - versionName "1.0.43" + versionCode 64 + versionName "1.0.44" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/db/admin-overrides.sql b/db/admin-overrides.sql index 059dc0d..cd88ef2 100644 --- a/db/admin-overrides.sql +++ b/db/admin-overrides.sql @@ -541,6 +541,15 @@ INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (11 INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1107, 883002, 10); INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1108, 883002, 10); INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1109, 883002, 10); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1201, 983003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1202, 983003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1203, 983003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1204, 1083003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1205, 1083003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1206, 1083003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1207, 1183003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1208, 1183003, 15); +INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1209, 1183003, 15); INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1301, 1283004, 20); INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1302, 1283004, 20); INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (1303, 1283004, 20); diff --git a/server/admin.mjs b/server/admin.mjs index 1b63dd4..bff7d54 100644 --- a/server/admin.mjs +++ b/server/admin.mjs @@ -375,6 +375,31 @@ const server = createServer(async (request, response) => { SELECT from_item_id AS fromItemId, to_item_id AS toItemId FROM gear_upgrade_paths ORDER BY from_item_id `).all() + const classes = database.prepare(` + SELECT id, slug, name, resource_name AS resourceName, + max_resource AS maxResource, theme_color AS themeColor, description + FROM classes ORDER BY id + `).all() + const abilities = database.prepare(` + SELECT id, class_id AS classId, slug, name, spell_type AS spellType, + resource_cost AS cost, cooldown_seconds AS cooldown, power, + unlock_level AS unlockLevel, glyph, description + FROM spells ORDER BY class_id, unlock_level, id + `).all() + const talents = database.prepare(` + SELECT talents.id, talents.class_id AS classId, talents.slug, talents.name, + talents.max_rank AS maxRank, talents.tier, talents.branch, + talents.prerequisite_talent_id AS prerequisiteTalentId, + talents.prerequisite_rank AS prerequisiteRank, + prerequisite.name AS prerequisiteName, + talents.effect_type AS effectType, + talents.effect_value_per_rank AS effectValuePerRank, + talents.glyph, talents.description + FROM talents + LEFT JOIN talents AS prerequisite + ON prerequisite.id = talents.prerequisite_talent_id + ORDER BY talents.class_id, talents.tier, talents.branch + `).all() sendJson(response, 200, { items, encounters, @@ -383,6 +408,11 @@ const server = createServer(async (request, response) => { craftingRecipes: [...recipes.values()], dungeons, gearUpgradePaths, + classes: classes.map((gameClass) => ({ + ...gameClass, + abilities: abilities.filter((ability) => ability.classId === gameClass.id), + talents: talents.filter((talent) => talent.classId === gameClass.id), + })), }) return } @@ -499,12 +529,14 @@ const server = createServer(async (request, response) => { const recipeComponents = request.url.match(/^\/api\/admin\/crafting-recipes\/(\d+)\/components$/) if (recipeComponents && request.method === 'POST') { const payload = await readJson(request) + const quantity = Number(payload.quantity) + if (!Number.isInteger(quantity) || quantity < 1) throw new Error('Component quantity must be at least 1.') database.prepare(` INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity) VALUES (?, ?, ?) ON CONFLICT(recipe_id, item_id) DO UPDATE SET quantity = excluded.quantity - `).run(Number(recipeComponents[1]), payload.itemId, payload.quantity) + `).run(Number(recipeComponents[1]), payload.itemId, quantity) writeAdminOverrides(database) sendJson(response, 200, { ok: true }) return diff --git a/server/game-api.mjs b/server/game-api.mjs index 1b81694..3b21170 100644 --- a/server/game-api.mjs +++ b/server/game-api.mjs @@ -858,6 +858,8 @@ export function getProfile(database, characterId, accountId) { quantity, owned, })) + const hasRequiredComponents = components.length > 0 + && components.every((component) => component.quantity > 0) const { itemId, slug, name, slot, rarity, itemLevel, healingPower, maxResourceBonus, glyph, description, setId, setSlug, setName } = recipe return { id: recipe.id, @@ -880,7 +882,8 @@ export function getProfile(database, characterId, accountId) { setName, }, components, - canCraft: components.every((component) => component.owned >= component.quantity), + canCraft: hasRequiredComponents + && components.every((component) => component.owned >= component.quantity), } }), dungeons: dungeons.map((dungeon) => ({ @@ -1746,6 +1749,9 @@ function craftItem(database, characterId, recipeId) { WHERE crafting_recipe_components.recipe_id = ? `).all(characterId, recipeId) if (components.length === 0) throw new Error('That recipe has no component requirements.') + if (components.some((component) => component.quantity <= 0)) { + throw new Error('Recipe components must require at least one item.') + } const missing = components.find((component) => component.owned < component.quantity) if (missing) { const item = itemById(database, missing.itemId) @@ -1845,6 +1851,10 @@ function upgradeItem(database, characterId, itemId) { AND character_inventory.character_id = ? WHERE crafting_recipe_components.recipe_id = ? `).all(characterId, targetRecipe.id) + if (components.length === 0) throw new Error('That upgrade has no component requirements.') + if (components.some((component) => component.quantity <= 0)) { + throw new Error('Upgrade components must require at least one item.') + } const missing = components.find((component) => component.owned < component.quantity) if (missing) { const componentItem = itemById(database, missing.itemId) diff --git a/src/App.css b/src/App.css index 59297a1..d37b9fa 100644 --- a/src/App.css +++ b/src/App.css @@ -2664,10 +2664,15 @@ h2 { .crafting-layout { gap: 6px; - grid-template-columns: minmax(134px, 0.42fr) minmax(248px, 1fr) minmax(190px, 0.72fr); + grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr); margin-top: 6px; } + .crafting-available-panel { + gap: 6px; + grid-template-columns: minmax(0, 1.25fr) minmax(170px, 0.75fr); + } + .crafting-filters { gap: 7px; } @@ -4154,13 +4159,14 @@ h2 { display: grid; gap: 12px; flex: 1; - grid-template-columns: minmax(210px, 0.55fr) minmax(360px, 1fr) minmax(320px, 0.85fr); + grid-template-columns: minmax(230px, 1fr) minmax(0, 2fr); margin-top: 13px; min-height: 0; overflow: hidden; } .crafting-filters, +.crafting-available-panel, .crafting-list-panel, .crafting-detail-panel { background: var(--panel-light); @@ -4177,10 +4183,20 @@ h2 { gap: 14px; } +.crafting-available-panel { + background: transparent; + border: 0; + display: grid; + gap: 12px; + grid-template-columns: minmax(360px, 1.05fr) minmax(280px, 0.95fr); + outline: 0; + padding: 0; +} + .crafting-filter-grid { display: grid; gap: 7px; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1fr); } .crafting-filter-grid button, @@ -6573,7 +6589,8 @@ h2 { .gear-summary, .equipment-layout, - .crafting-layout { + .crafting-layout, + .crafting-available-panel { grid-template-columns: 1fr; } @@ -7244,9 +7261,203 @@ h2 { margin-left: auto; } +.admin-class-layout { + display: grid; + gap: 14px; + grid-template-columns: minmax(220px, 0.35fr) minmax(0, 1fr); +} + +.admin-class-list { + background: #1c1e25; + border: 2px solid #090a0d; + display: flex; + flex-direction: column; + gap: 8px; + outline: 2px solid #494754; + padding: 12px; +} + +.admin-class-list button { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + display: grid; + gap: 9px; + grid-template-columns: 38px 1fr; + min-height: 54px; + outline: 2px solid #41404a; + padding: 8px; + text-align: left; +} + +.admin-class-list button.active, +.admin-class-list button:hover { + outline-color: var(--class-color, var(--gold)); +} + +.admin-class-list button > span, +.admin-class-hero > span { + align-items: center; + background: var(--class-color, var(--gold)); + color: #111217; + display: flex; + font-family: 'Press Start 2P', monospace; + font-size: 13px; + height: 38px; + justify-content: center; +} + +.admin-class-list strong, +.admin-class-list small { + display: block; +} + +.admin-class-list small { + color: var(--muted); + font-size: 12px; + margin-top: 3px; +} + +.admin-class-detail { + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; +} + +.admin-class-hero { + align-items: center; + background: var(--panel-light); + border: 2px solid #090a0d; + display: grid; + gap: 12px; + grid-template-columns: 54px minmax(180px, auto) minmax(0, 1fr); + outline: 2px solid #494754; + padding: 12px; +} + +.admin-class-hero > span { + height: 54px; +} + +.admin-class-hero h2 { + font-family: 'Press Start 2P', monospace; + font-size: 13px; +} + +.admin-class-hero small, +.admin-class-hero p { + color: var(--muted); + font-size: 14px; +} + +.admin-class-table { + display: grid; + gap: 6px; +} + +.admin-class-table-head, +.admin-class-row { + align-items: center; + display: grid; + gap: 8px; + grid-template-columns: minmax(230px, 1.5fr) minmax(90px, 0.7fr) minmax(110px, 0.6fr) minmax(65px, 0.45fr) minmax(80px, 0.5fr) minmax(70px, 0.45fr); +} + +.admin-class-table-head { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + padding: 0 10px; + text-transform: uppercase; +} + +.admin-class-row { + background: var(--panel-light); + border: 2px solid #090a0d; + outline: 2px solid #494754; + padding: 9px 10px; +} + +.admin-class-row > span { + color: var(--muted); + font-size: 13px; +} + +.admin-class-row > span:first-child { + align-items: center; + display: grid; + gap: 8px; + grid-template-columns: 30px minmax(0, 1fr); +} + +.admin-class-row i { + color: var(--gold); + font-style: normal; + text-align: center; +} + +.admin-class-row strong, +.admin-class-row small { + display: block; +} + +.admin-class-row small { + color: var(--muted); + font-size: 11px; + margin-top: 3px; +} + +.admin-class-talent-grid { + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} + +.admin-class-talent { + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + flex-direction: column; + gap: 6px; + outline: 2px solid #494754; + padding: 10px; +} + +.admin-class-talent > div { + align-items: center; + display: flex; + gap: 8px; +} + +.admin-class-talent > div > span { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + width: 26px; +} + +.admin-class-talent small, +.admin-class-talent p { + color: var(--muted); + font-size: 12px; +} + +.admin-class-talent em { + color: var(--green); + font-family: 'Press Start 2P', monospace; + font-size: 7px; + font-style: normal; +} + @media (max-width: 800px) { .admin-upgrade-toolbar, - .admin-upgrade-step { + .admin-upgrade-step, + .admin-class-layout, + .admin-class-hero, + .admin-class-table-head, + .admin-class-row { grid-template-columns: 1fr; } @@ -7678,15 +7889,18 @@ h2 { .workshop-shell .crafting-layout { gap: 6px; - grid-template-columns: minmax(0, 1fr); - grid-template-rows: auto minmax(0, 1fr); + grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr); margin-top: 6px; } - .workshop-shell .crafting-filters { - display: grid; + .workshop-shell .crafting-available-panel { + gap: 6px; + grid-template-columns: minmax(0, 1fr); + } + + .workshop-shell .crafting-filters { + display: flex; gap: 6px; - grid-template-columns: minmax(0, 1fr) auto; } .workshop-shell .crafting-filter-grid, @@ -7695,7 +7909,7 @@ h2 { } .workshop-shell .crafting-filter-grid { - grid-template-columns: repeat(10, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1fr); } .workshop-shell .crafting-filter-grid button { @@ -7985,7 +8199,7 @@ h2 { } .workshop-shell .crafting-layout { - grid-template-columns: 110px minmax(0, 1fr) 174px; + grid-template-columns: 110px minmax(0, 1fr); } .workshop-shell .crafting-filter-grid { @@ -8189,7 +8403,7 @@ h2 { grid-template-columns: minmax(0, 1fr); } - .workshop-shell .crafting-filters { + .workshop-shell .crafting-available-panel { grid-template-columns: minmax(0, 1fr); } diff --git a/src/components/AdminScreen.tsx b/src/components/AdminScreen.tsx index 20604b6..68cae44 100644 --- a/src/components/AdminScreen.tsx +++ b/src/components/AdminScreen.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import type { Dispatch, SetStateAction } from 'react' +import type { CSSProperties, Dispatch, SetStateAction } from 'react' type AdminItem = { id: number @@ -76,6 +76,49 @@ type AdminUpgradePath = { toItemId: number } +type AdminAbility = { + id: number + classId: number + slug: string + name: string + spellType: string + cost: number + cooldown: number + power: number + unlockLevel: number + glyph: string + description: string +} + +type AdminTalent = { + id: number + classId: number + slug: string + name: string + maxRank: number + tier: number + branch: number + prerequisiteTalentId: number | null + prerequisiteRank: number + prerequisiteName: string | null + effectType: string + effectValuePerRank: number + glyph: string + description: string +} + +type AdminClass = { + id: number + slug: string + name: string + resourceName: string + maxResource: number + themeColor: string + description: string + abilities: AdminAbility[] + talents: AdminTalent[] +} + type AdminData = { items: AdminItem[] encounters: AdminEncounter[] @@ -84,9 +127,10 @@ type AdminData = { craftingRecipes: AdminRecipe[] dungeons: AdminDungeon[] gearUpgradePaths: AdminUpgradePath[] + classes: AdminClass[] } -type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades' +type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades' | 'classes' type SavingState = Record type SetData = Dispatch> type SetSaving = Dispatch> @@ -99,6 +143,7 @@ const tabs: { id: AdminTab; label: string }[] = [ { id: 'loot', label: 'Loot' }, { id: 'crafting', label: 'Crafting' }, { id: 'upgrades', label: 'Upgrades' }, + { id: 'classes', label: 'Classes' }, ] async function fetchJson(url: string, init?: RequestInit): Promise { @@ -143,6 +188,7 @@ export function AdminScreen({ onBack }: { onBack: () => void }) { {tab === 'loot' && } {tab === 'crafting' && } {tab === 'upgrades' && } + {tab === 'classes' && } ) } @@ -830,7 +876,9 @@ function CraftingTab({ data, setData, setSaving, saving }: { )}

Required Components

- {(!recipe || recipe.components.length === 0) &&

No component requirements.

} + {(!recipe || recipe.components.length === 0) && ( +

No component requirements. Crafting and upgrades are blocked until materials are added.

+ )}
{recipe?.components.map((comp) => (
@@ -981,7 +1029,7 @@ function UpgradesTab({ data, setData, setSaving, saving }: { {target ? `Target requirements: ${targetRecipe && targetRecipe.components.length > 0 ? targetRecipe.components.map((component) => `${component.quantity}x ${itemName(data, component.itemId)}`).join(', ') - : 'none'}` + : 'none configured - upgrade blocked until materials are added'}` : 'No next upgrade selected.'}

@@ -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 ( +
+
+ + + {selectedClass ? ( +
+
+ {selectedClass.name[0]} +
+

{selectedClass.slug}

+

{selectedClass.name}

+ {selectedClass.resourceName} pool: {selectedClass.maxResource} +
+

{selectedClass.description}

+
+ +
+

Abilities ({selectedClass.abilities.length})

+
+
+ Ability + Type + Default Strength + Cost + Cooldown + Unlock +
+ {selectedClass.abilities.map((ability) => ( +
+ {ability.glyph}{ability.name}{ability.description} + {ability.spellType} + {ability.power} + {ability.cost} + {ability.cooldown}s + Lvl {ability.unlockLevel} +
+ ))} +
+
+ +
+

Talents ({selectedClass.talents.length})

+
+ {selectedClass.talents.map((talent) => ( +
+
+ {talent.glyph} + {talent.name} +
+ Tier {talent.tier} · Branch {talent.branch} · Max {talent.maxRank} +

{talent.description}

+ {talent.effectType}: {talent.effectValuePerRank}/rank + {talent.prerequisiteName && ( + Requires {talent.prerequisiteName} rank {talent.prerequisiteRank} + )} +
+ ))} +
+
+
+ ) : ( +

No classes found.

+ )} +
+
+ ) +} + function jsonRequest(method: 'POST' | 'PUT', body: unknown): RequestInit { return { method, diff --git a/src/components/EquipmentScreen.tsx b/src/components/EquipmentScreen.tsx index fe4a6b5..4082ad1 100644 --- a/src/components/EquipmentScreen.tsx +++ b/src/components/EquipmentScreen.tsx @@ -596,8 +596,12 @@ export function EquipmentScreen({ />
-
- - {filteredRecipes.length === 0 ? ( -

No recipes match filters.

- ) : ( -
- {recipePageItems.map((recipe) => ( - - ))} -
- )} +
+
+ + {filteredRecipes.length === 0 ? ( +

No recipes match filters.

+ ) : ( +
+ {recipePageItems.map((recipe) => ( + + ))} +
+ )} {filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && ( )} -
- -
-
- -
- {selectedRecipe ? ( -
- -
-

Materials

- {selectedRecipe.canCraft ? 'Ready' : 'Missing components'} -
-
- {selectedRecipe.components.map((component) => ( -
= component.quantity ? 'ready' : 'missing'} - key={component.item.id} - > - {component.item.glyph} - {component.item.name} - {component.owned}/{component.quantity} -
- ))} -
+
+
- ) : ( -

Select a recipe.

- )} +
+ +
+ {selectedRecipe ? ( +
+ +
+

Materials

+ {selectedRecipe.canCraft ? 'Ready' : 'Missing components'} +
+
+ {selectedRecipe.components.length === 0 && ( +

No materials configured. Crafting disabled.

+ )} + {selectedRecipe.components.map((component) => ( +
= component.quantity ? 'ready' : 'missing'} + key={component.item.id} + > + {component.item.glyph} + {component.item.name} + {component.owned}/{component.quantity} +
+ ))} +
+
+ ) : ( +

Select a recipe.

+ )} +
diff --git a/src/gameRepository.ts b/src/gameRepository.ts index 2f05856..3fbf340 100644 --- a/src/gameRepository.ts +++ b/src/gameRepository.ts @@ -364,10 +364,13 @@ function updateCraftingRecipes(profile: CharacterProfile) { ...component, owned: owned.get(component.item.id) ?? 0, })) + const hasRequiredComponents = components.length > 0 + && components.every((component) => component.quantity > 0) return { ...recipe, components, - canCraft: components.every((component) => component.owned >= component.quantity), + canCraft: hasRequiredComponents + && components.every((component) => component.owned >= component.quantity), } }) } @@ -1301,12 +1304,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository { if (!recipe) throw new Error('That crafting recipe does not exist.') const requiresUpgrade = !DIRECT_CRAFT_ITEM_LEVELS.has(recipe.item.itemLevel) if (requiresUpgrade) throw new Error('Upgrade the previous item tier instead.') + if (recipe.components.length === 0) throw new Error('That recipe has no component requirements.') const missing = recipe.components.find((component) => component.owned < component.quantity) if (missing) { throw new Error(`Need ${missing.quantity} ${missing.item.name} to craft this item.`) } for (const component of recipe.components) { + if (component.quantity <= 0) throw new Error('Recipe components must require at least one item.') const owned = profile.inventory.find((candidate) => candidate.id === component.item.id) if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to craft this item.`) owned.quantity -= component.quantity @@ -1331,12 +1336,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository { ? selectUpgradeRecipe(profile.gearUpgradePaths ?? [], profile.craftingRecipes, item) : null if (!targetRecipe) throw new Error('No upgrade is available for this item.') + if (targetRecipe.components.length === 0) throw new Error('That upgrade has no component requirements.') const missing = targetRecipe.components.find((component) => component.owned < component.quantity) if (missing) { throw new Error(`Need ${missing.quantity} ${missing.item.name} to upgrade this item.`) } for (const component of targetRecipe.components) { + if (component.quantity <= 0) throw new Error('Upgrade components must require at least one item.') const owned = profile.inventory.find((candidate) => candidate.id === component.item.id) if (!owned) throw new Error(`Need ${component.quantity} ${component.item.name} to upgrade this item.`) owned.quantity -= component.quantity diff --git a/src/offline-starter-profile.json b/src/offline-starter-profile.json index edf0b38..56acaa8 100644 --- a/src/offline-starter-profile.json +++ b/src/offline-starter-profile.json @@ -1797,7 +1797,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1203, @@ -1820,7 +1820,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1201, @@ -1843,7 +1843,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1204, @@ -1866,7 +1866,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1205, @@ -1889,7 +1889,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1206, @@ -1912,7 +1912,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1209, @@ -1935,7 +1935,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1208, @@ -1958,7 +1958,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1207, @@ -1981,7 +1981,7 @@ "setName": null }, "components": [], - "canCraft": true + "canCraft": false }, { "id": 1302,