diff --git a/src/App.svelte b/src/App.svelte
index 4059189c..0cd62ff9 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -503,6 +503,7 @@ Anyway?`,
{t("Achievements")} /
{t("Conducts")}
+
{t("Skills")}
{t("Proficiencies")}
diff --git a/src/types/Skill.svelte b/src/types/Skill.svelte
index b3b31209..b7bfb819 100644
--- a/src/types/Skill.svelte
+++ b/src/types/Skill.svelte
@@ -48,6 +48,102 @@ const itemsUsingSkill = data
) as SupportedTypesWithMapped["GUN"][];
itemsUsingSkill.sort(byName);
+const craftingRecipes = data
+ .byType("recipe")
+ .filter(
+ (r) =>
+ r.skill_used === item.id &&
+ r.result &&
+ data.byIdMaybe("item", r.result) &&
+ !r.never_learn
+ );
+
+const recipesByLevel = new Map();
+for (const recipe of craftingRecipes) {
+ const level = recipe.difficulty ?? 0;
+ if (!recipesByLevel.has(level)) recipesByLevel.set(level, []);
+ recipesByLevel.get(level)!.push(recipe);
+}
+const recipesByLevelList = [...recipesByLevel.entries()].sort(
+ (a, b) => a[0] - b[0]
+);
+recipesByLevelList.forEach(([, recipes]) => {
+ recipes.sort((a, b) => {
+ const itemA = data.byId("item", a.result!);
+ const itemB = data.byId("item", b.result!);
+ if (!itemA || !itemB) return 0; // If either item doesn't exist, consider them equal
+ return singularName(itemA).localeCompare(singularName(itemB));
+ });
+});
+
+function getRecipeLearningInfo(recipe: any, recipeLevel: number): string {
+ const parts = [];
+
+ // Check for autolearn
+ if (recipe.autolearn) {
+ if (Array.isArray(recipe.autolearn)) {
+ // Multiple skills for autolearn - check if it's just current skill at current level
+ if (
+ recipe.autolearn.length === 1 &&
+ recipe.autolearn[0][0] === item.id &&
+ recipe.autolearn[0][1] === recipeLevel
+ ) {
+ parts.push("Autolearn");
+ } else {
+ const autolearns = recipe.autolearn.map(
+ ([skill, level]: [string, number]) => {
+ const skillData = data.byIdMaybe("skill", skill);
+ const skillName = skillData ? singularName(skillData) : skill;
+ return `${skillName} ${level}`;
+ }
+ );
+ parts.push(`Autolearn ${autolearns.join(", ")}`);
+ }
+ } else {
+ // Single skill autolearn based on recipe difficulty
+ if (recipe.skill_used) {
+ const recipeDifficulty = recipe.difficulty ?? 0;
+ // If autolearn skill matches current skill and level matches the group level, just say "Autolearn"
+ if (recipe.skill_used === item.id && recipeDifficulty === recipeLevel) {
+ parts.push("Autolearn");
+ } else {
+ const skillData = data.byIdMaybe("skill", recipe.skill_used);
+ const skillName = skillData
+ ? singularName(skillData)
+ : recipe.skill_used;
+ parts.push(`Autolearn ${skillName} ${recipeDifficulty}`);
+ }
+ }
+ }
+ }
+
+ // Check for book learning
+ const writtenIn = Array.isArray(recipe.book_learn)
+ ? [...recipe.book_learn]
+ : [...Object.entries((recipe.book_learn ?? {}) as Record)].map(
+ ([k, v]) => [k, v.skill_level ?? v]
+ );
+
+ if (writtenIn.length > 0) {
+ if (writtenIn.length >= 3) {
+ parts.push(`Written in ${writtenIn.length} books`);
+ } else {
+ const bookNames = writtenIn.map(([bookId, level]) => {
+ const book = data.byIdMaybe("item", bookId);
+ const bookName = book ? singularName(book) : bookId;
+ return level ? `${bookName} (${level})` : bookName;
+ });
+ if (writtenIn.length === 2) {
+ parts.push(`Written in ${bookNames.join(" and ")}`);
+ } else {
+ parts.push(`Written in ${bookNames[0]}`);
+ }
+ }
+ }
+
+ return parts.length > 0 ? ` (${parts.join("; ")})` : "";
+}
+
const practiceRecipes = data
.byType("practice")
.filter((r) => r.skill_used === item.id);
@@ -91,6 +187,26 @@ practiceRecipes.sort(
{/if}
+{#if craftingRecipes.length}
+
+ {t("Crafting Recipes", { _context: "Skill" })}
+
+ {#each recipesByLevelList as [level, recipes]}
+ - Level {level}
+ -
+
+ {#if item.result}
+ {getRecipeLearningInfo(item, level)}
+ {/if}
+
+
+ {/each}
+
+
+{/if}
+
{#if practiceRecipes.length}
{t("Practice Recipes", { _context: "Skill" })}
{#each practiceRecipes as recipe}