Initial commit

This commit is contained in:
2026-08-07 15:43:16 -06:00
parent 0a799b94d1
commit e48c0c1f38
84 changed files with 37547 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
### v1.0 (2026/08/06)
- Initial release: from-scratch rebuild of MissingCrafts v1.4.1 with no Ace3 dependency (AceAddon/AceDB/AceGUI/AceHook/AceLocale all replaced with native vanilla frames and a small local helper layer) to avoid the cross-addon conflicts Ace3 causes on OctoWoW/Turtle WoW
- Add support for Turtle WoW's Survival profession (starter recipe data set — see README's "Survival profession data" section)
- Add `/gmc dump` developer command to help complete the Survival (and any other profession's) recipe data from the live client
- Keep pfUI window placement support
- Rebuild every interactive widget without Blizzard UI templates and with explicit frame strata, after pfUI's skinning repeatedly broke click handling on templated widgets
- Rebuild the recipe list on a fixed-size row pool with a custom scrollbar (drag, click, and mouse wheel) instead of a native ScrollFrame, which wasn't clipping its content reliably on this client
- The window now anchors directly to the profession frame it's attached to and moves with it automatically, instead of being independently draggable
- Remove the profession/character filter dropdowns — the window is already scoped to whichever profession frame it's open from, and always your current character; search by recipe name instead
- Fix `/gmc` slash command never registering (it was writing SLASH_* into the addon's isolated environment instead of the real global namespace the client actually scans)
- Expand the Survival starter recipe set to 7 confirmed recipes (Dim Torch, Survivalist's Skinning Knife, Driftwood Fishing Pole, Traveler's Tent, Fishing Boat, Murloc's Flippers, Repaired Electro-Lantern) using a full player-provided skill-up table cross-checked against octowow.st's database; corrected two skill levels that earlier web research had gotten wrong (Traveler's Tent 50→80, Fishing Boat 125→115)
- Add `/gmc dump` support for the trainer window (lists every recipe on offer, not just ones already known)
- Expand Survival to 44 recipes (skill 1-150) using real names/skill levels/result item ids from a player-provided `/gmc dump`; recipes without a confirmed real spell id use a safe placeholder instead (see README's "Survival profession data" section) — also corrected Traveler's Tent's result item (51283, not 50234, which turned out to be an unrelated "outline" item)
- Fix `/gmc dump` not detecting the trainer window on this client (it's `ClassTrainerFrame`, not the vanilla-standard `TrainerFrame`) — the command now also self-diagnoses (reports what it did/didn't find) when nothing matches, instead of just failing silently
- Expand Survival to the full 87-recipe list (skill 1-300), using a trainer-service-list `/gmc dump` to confirm every remaining recipe's name and skill level
- Fill in real result item ids for all 87 Survival recipes (previously 44 had one), sourced by hand from octowow.st's database; also corrected Iron Lantern's item (2714, not 81187, which turned out to be an unrelated "outline" item — same category of mistake as the earlier Traveler's Tent fix)
+104
View File
@@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Gromissingcrafts is a World of Warcraft Classic (Turtle WoW/OctoWoW) addon that helps players track crafting recipes they haven't learned yet on their current character. It is a from-scratch, Ace3-free rebuild of [MissingCrafts](https://github.com/refaim/MissingCrafts), built because Turtle-style 1.12 clients frequently break when multiple addons each bundle their own copy of Ace3 (AceAddon/AceDB/AceGUI/AceHook/AceLocale share a single global registry, so mismatched versions/instances taint or clobber each other). Everything here is built on native `CreateFrame` + manual scripting, with no Blizzard UI templates and no Ace3.
## Architecture
### Environment Layer
- **`src/Environment.lua`**: Creates an isolated global environment using `setfenv(1, Gromissingcrafts)` that prevents global namespace pollution while maintaining access to WoW API
- Every `.lua` file must begin with `setfenv(1, Gromissingcrafts)` to work within this isolated environment
- Provides utility functions: `erase()` for table cleanup, `tpop()` for safe key removal
### Database Layer (`src/db/`)
Repository pattern implementation with clean domain models:
- **`Database.lua`**: Manages the `GromissingcraftsDatabase` SavedVariable directly (no AceDB) via a small get-or-create helper that auto-vivifies `global.realmToNameToCharacter[realm][name]` with default fields
- **`Character.lua`**: Domain model representing a character with profession knowledge and skill level checking
- **`CharacterRepository.lua`**: Data access layer for character CRUD operations
- **`CraftRepository.lua`**: Complex repository finding missing recipes based on character filters, handles craft sources mapping
### UI Layer (`src/ui/`)
Native-frame component system, no widget framework, and **no Blizzard UI templates on anything interactive** (`UIDropDownMenuTemplate`, `UIPanelButtonTemplate`, `InputBoxTemplate`, `UIPanelScrollFrameTemplate` were all tried and all got their input handling broken by pfUI's skinning — see git history). Every interactive frame is hand-built (manual backdrop/textures) and every one of them sets `SetFrameStrata("DIALOG")` explicitly, because **strata is not inherited from a parent frame** on this client — leaving it unset was silently losing click priority to whatever else was on screen underneath.
- **`UIManager.lua`**: Central coordinator managing window lifecycle, profession frame events, and component interactions. Hooks `MovePanelToLeft`/`MovePanelToCenter` via the local `hookGlobalFunction` helper (see `ui/Tools.lua`) — **not** the native `hooksecurefunc`, which isn't reliably present on true-1.12-era clients
- **`window/Window.lua`**: Main window — a plain `CreateFrame("Frame", ..., UIParent)` with a `DialogBox` backdrop, fixed `"DIALOG"` strata, **anchored directly to the profession frame** (`SetPoint("TOPLEFT", professionFrame.widget, "TOPRIGHT", ...)`) so it moves with it automatically via WoW's native anchor propagation — no drag support of its own, not meant to be repositioned independently
- **`filters/FiltersPanel.lua`**: Plain frame holding just a search box (the profession/character dropdown filters were removed by request — the window is already scoped to whichever profession frame it's attached to, and always the current character)
- **`filters/SearchField.lua`**: Hand-built `EditBox`, no template
- **`list/CraftsList.lua`**: **Not** a native `ScrollFrame` + `SetScrollChild` — that combination wasn't clipping its content reliably on this client (rows kept spilling past the frame's own height regardless of template). Instead it's a fixed pool of row widgets sized to exactly fill the visible area (the same technique Blizzard's own `FauxScrollFrame`-based lists use), with a scroll offset into the data. The scrollbar (bigger than Blizzard's default, click-and-drag capable) is fully custom too, built the same template-free way.
- **`list/CraftsListItem.lua`**: Individual recipe row — always was plain frames, unchanged from upstream
- **`TooltipEnhancer.lua`**: Enhances item tooltips with recipe learning status for each character (via `LibItemTooltip`, which was never Ace-based)
- **`window/OpenButton.lua`**: Toggle button that appears on profession frames to show/hide the main window
- **`DataStateManager.lua`**: State management for UI data synchronization and event coordination — tracks current character/profession/search query and re-queries `CraftRepository` on change
- **`VanillaFramePool.lua`**: Efficient frame reuse system for performance optimization
- **`window/PlacementPolicy.lua`**: Geometry/anchor rules per profession-frame type (now relative offsets from the profession frame's `TOPRIGHT`, not absolute screen coordinates), including pfUI detection (`IsAddOnLoaded("pfUI")`)
### Localization (`src/locale/`)
- **`Tools.lua`**: Minimal AceLocale-free replacement — one shared `L` table. `enUS.lua` loads **first** (see the `.toc` order) and populates every key as the fallback; every other locale file only receives the live table (and thus only gets to override keys) when `GetLocale()` matches its own code
- Multiple language files (enUS, ruRU, deDE, frFR, esES, ptBR, koKR, zhCN, zhTW)
### Dev tooling
- **`src/DevTools.lua`**: `/gromissingcrafts` (alias `/gmc`) slash command. `dump` walks whichever trade skill/craft window is open (`GetTradeSkillInfo`/`GetCraftInfo` + item-link parsing) and prints each recipe's exact name and result item id, since neither spell id nor required skill level is exposed by the client API for known recipes. Used to grow `lib/LibCrafts-1.0/Professions/Turtle/Survival.lua` with confirmed data instead of guesses.
## Key Technical Features
### Event-Driven Architecture
- Uses LibCraftingProfessions events (`LCP_SKILLS_UPDATE`, `LCP_FRAME_SHOW`, `LCP_FRAME_CLOSE`) for real-time updates
- Automatic profession data synchronization when skills are learned
- Dynamic UI updates when profession frames open/close
### Data Persistence
- Cross-realm character data storage with automatic cleanup, stored directly in `GromissingcraftsDatabase`
- Profession knowledge tracking with skill level requirements
- Recipe source mapping from LibCrafts constants to internal enums
## External Dependencies
### Kept from MissingCrafts (none of these were ever Ace3-dependent)
- **LibStub**: tiny library-versioning shim, not part of the Ace3 conflict surface
- **LibCraftingProfessions-1.0**: Profession frame detection and skill tracking across multiple addon types (extended here with `Survival`)
- **LibCrafts-1.0**: Comprehensive crafting recipe database with source information (extended here with `Professions/Turtle/Survival.lua`)
- **LibItemTooltip-1.0**: Tooltip enhancement framework (hooks `GameTooltip`/`ItemRefTooltip` methods directly)
### Removed entirely
- AceAddon-3.0, AceDB-3.0, AceGUI-3.0, AceHook-3.0, AceLocale-3.0 — see `README.md` and the plan history for the replacement mapping
## Supported Profession Addons
- Default WoW profession frames
- AdvancedTradeSkillWindow (ATSW)
- AdvancedTradeSkillWindow2 (ATSW2)
- Artisan
- pfUI
Different frame types require different initialization delays and positioning strategies — see `PlacementPolicy.lua`.
## Development Guidelines
### Code Style
- Extensive use of Lua `---@` type annotations for documentation and IDE support
- Object-oriented patterns using metatables with `self` parameters
- Acquire/release pattern for UI components to prevent memory leaks
- Repository pattern for clean data access abstraction
### File Loading Order
Files must load in the order given in `Gromissingcrafts.toc`:
1. `LibStub` + the three data libraries
2. `src/Environment.lua`, `src/Compatibility.lua`
3. Database layer files
4. Locale files — **`enUS.lua` must stay first** among locale files (see `locale/Tools.lua`)
5. UI component files
6. `src/DevTools.lua`
7. `src/Gromissingcrafts.lua` — main addon bootstrap (plain `ADDON_LOADED` event frame, no AceAddon)
### No live-client testing available in this environment
Changes here are verified statically (Lua syntax checks, manual trace-throughs, cross-checking renamed globals). Real acceptance testing has to happen in-game on OctoWoW/Turtle WoW.
### Lessons learned the hard way (read before touching UI code)
- **No Blizzard UI templates on interactive frames.** pfUI (and likely other skinning addons) hook/reskin recognized templates and this repeatedly broke click handling for dynamically-created widgets — first `UIDropDownMenuTemplate`, then `UIPanelButtonTemplate`, then `InputBoxTemplate`. Build interactive widgets from bare `CreateFrame` + manual backdrop/textures instead (see `Dropdown` history, `SearchField.lua`, `OpenButton.lua`).
- **Set `SetFrameStrata` explicitly on every interactive frame**, not just the top-level window. It is not inherited from a parent in this client; an unset child silently defaults to `"MEDIUM"` and can lose click priority to something else on screen even while it visually renders on top.
- **Native `ScrollFrame` + `SetScrollChild` did not reliably clip its content** on this client, template or no template. `CraftsList.lua` uses a fixed-size row pool instead (see that file's header comment).
- **Anchor to the frame you want to track, not to a computed absolute position.** The window used to snapshot the profession frame's screen coordinates once and reposition on a few known hooks; now it's anchored directly via `SetPoint(..., professionFrame.widget, ...)` so it tracks automatically, including drags, with no extra code.
+48
View File
@@ -0,0 +1,48 @@
## Interface: 11200
## Title: Gromissingcrafts
## Notes: Shows the missing crafts for your crafting professions and how to obtain them. Ace3-free, pfUI-friendly fork of MissingCrafts with Survival profession support.
## Author: Refaim, Grom
## Version: 1.0
## SavedVariables: GromissingcraftsDatabase
## OptionalDeps: AdvancedTradeSkillWindow, AdvancedTradeSkillWindow2, Artisan, pfUI
lib\LibStub\LibStub.lua
lib\LibCraftingProfessions-1.0\LibCraftingProfessions-1.0.xml
lib\LibCrafts-1.0\LibCrafts-1.0.xml
lib\LibItemTooltip-1.0\LibItemTooltip-1.0.xml
src\Environment.lua
src\Compatibility.lua
src\db\Database.lua
src\db\Character.lua
src\db\CharacterRepository.lua
src\db\CraftRepository.lua
src\locale\Tools.lua
src\locale\enUS.lua
src\locale\deDE.lua
src\locale\esES.lua
src\locale\frFR.lua
src\locale\koKR.lua
src\locale\ptBR.lua
src\locale\ruRU.lua
src\locale\zhCN.lua
src\locale\zhTW.lua
src\ui\Tools.lua
src\ui\VanillaFramePool.lua
src\ui\list\CraftsListItem.lua
src\ui\list\CraftsList.lua
src\ui\filters\SearchField.lua
src\ui\filters\FiltersPanel.lua
src\ui\window\PlacementPolicy.lua
src\ui\window\OpenButton.lua
src\ui\window\Window.lua
src\ui\TooltipEnhancer.lua
src\ui\DataStateManager.lua
src\ui\UIManager.lua
src\DevTools.lua
src\Gromissingcrafts.lua
+26
View File
@@ -0,0 +1,26 @@
MIT License
Copyright (c) 2025 Roman Kharitonov
Copyright (c) 2026 Grom (Gromissingcrafts changes)
Gromissingcrafts is a derivative work of MissingCrafts (https://github.com/refaim/MissingCrafts)
by Roman Kharitonov, rebuilt without the Ace3 framework and with added Survival
profession support, distributed under the same license below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+52
View File
@@ -0,0 +1,52 @@
# Gromissingcrafts
A World of Warcraft addon for Turtle WoW (and OctoWoW) that helps players track crafting recipes they haven't learned yet across their characters.
Gromissingcrafts is a from-scratch rebuild of [MissingCrafts](https://github.com/refaim/MissingCrafts) with **no Ace3** anywhere in the addon — Ace3's embedded framework (AceAddon/AceDB/AceGUI/AceHook/AceLocale) is a common source of cross-addon conflicts on non-standard 1.12 clients like Turtle WoW/OctoWoW, since every addon that bundles its own copy shares the same global Ace3 registry. Gromissingcrafts is built entirely on native vanilla WoW frames instead, and adds support for Turtle WoW's custom **Survival** profession.
## Features
- **Integrated Profession Window**: Attaches directly to the profession frame you have open, and moves with it
- **Enhanced Item Tooltips**: Shows which of your characters can learn recipes from items and their current learning status
- **Search**: Filter the current profession's missing recipes by name
- **Recipe Source Information**: Displays how to obtain each missing recipe (Vendor, Quest, Drop, etc.) where known
- **Multiple Profession Frame Support**: Works with default WoW frames and popular profession addons
- **All Vanilla Crafting Professions**, plus Turtle WoW's Jewelcrafting, Disguise, and **Survival**
- **pfUI-friendly**: Detects pfUI and anchors/sizes itself accordingly
- **No Ace3**: nothing here can collide with another addon's bundled Ace3 libraries
- **Multi-Language Support**: English, German, French, Spanish, Portuguese, Russian, Korean, Chinese (Simplified), Chinese (Traditional)
## How to Use
1. Open any crafting profession window
2. Click the recipe button in the upper right corner of the profession frame
3. The Gromissingcrafts window opens attached to it, showing every recipe for that profession you haven't learned on your current character
4. Type in the search box to filter by recipe name
5. Hover over recipes to see their source information
## Survival profession data
Turtle WoW's Survival profession has no public numeric recipe database that could be reliably scraped for this addon, so `lib/LibCrafts-1.0/Professions/Turtle/Survival.lua` is built entirely from a real player's own data: a skill-up table, a `/gmc dump` of their known trade skill entries (real result item ids), and a `/gmc dump` of both trainer NPCs' full service lists (every recipe on offer, known or not). Between the three, it's now the **full recipe list, skill 1 through 300** — 87 recipes. The only known gap is two skill-295 items taught by a recipe item rather than the trainer directly (Oil-Powered Cooker, Prospector's Magnifying Lens), left out rather than mis-tagged.
**All 87 now have a real result item id** — the last 43 (skill 155-300) were confirmed by hand against octowow.st's database. 7 recipes also have a confirmed real spell id; the other 80 use a safe placeholder (900001+, nowhere near any real spell id range) since the client API has no way to read a recipe's actual spell id under any window — only its name and, for known trade-skill entries, its result item. This doesn't affect accuracy: the addon matches "known vs. missing" by recipe **name**, and the recipe tooltip always prefers a real result item id over the spell id, which every entry now has — a placeholder spell id never actually surfaces anywhere.
The one remaining gap: two skill-295 items (Oil-Powered Cooker, Prospector's Magnifying Lens) are taught by a recipe item rather than the trainer directly, and neither their spell id nor item id is confirmed, so they're left out rather than mis-tagged.
If you ever want to confirm one of the placeholder spell ids for real: open the Survival trade skill window in-game once you've learned it and run `/gmc dump`, or look it up directly at `octowow.st/db/?spell=<id>` if you find one — **grab the Item ID, not the Display ID shown on the same page**, they're different numbers.
## Supported Profession Windows
- Default WoW profession windows
- Turtle WoW improved Trade Skill window
- [**AdvancedTradeSkillWindow**](https://github.com/laytya/AdvancedTradeSkillWindow-vanilla)
- [**AdvancedTradeSkillWindow2**](https://github.com/Shellyoung/AdvancedTradeSkillWindow2)
- [**Artisan**](https://github.com/Otari98/Artisan)
- [**pfUI**](https://github.com/shagu/pfUI)'s trade skill frame styling
## Relationship to MissingCrafts
Gromissingcrafts started as a straight port of [MissingCrafts](https://github.com/refaim/MissingCrafts) v1.4.1, replacing every Ace3-backed piece with a native equivalent and adding Survival. `LibCrafts-1.0`, `LibCraftingProfessions-1.0`, and `LibItemTooltip-1.0` — the three data/plumbing libraries MissingCrafts already built on top of — were never Ace3-dependent to begin with, so they carry over largely unchanged (plus the Survival additions above).
## Acknowledgments
- Built on top of [MissingCrafts](https://github.com/refaim/MissingCrafts) by Roman Kharitonov
- Inspired by [MissingTradeSkillsList](https://github.com/refaim/MissingTradeSkillsList)
@@ -0,0 +1,872 @@
--[[
Name: LibCraftingProfessions-1.0
Developed by: Refaim (rkharito@yandex.ru, https://github.com/refaim/)
Website: https://github.com/refaim/LibCraftingProfessions-1.0
Description: A library designed to provide a universal interface for crafting professions.
Dependencies: LibStub
Compatibility: World of Warcraft Vanilla (1.12.1), Turtle (1.18.0)
]]
---@type LibStubDef
local LibStub = getglobal("LibStub")
assert(LibStub ~= nil)
local untyped_lib, _ = LibStub:NewLibrary("LibCraftingProfessions-1.0", 19)
if not untyped_lib then
return
end
---@class LibCraftingProfessions
---@field event_to_handlers table<string, function[]>
---@field event_frame Frame
local lib = --[[---@type LibCraftingProfessions]] untyped_lib
if lib.event_to_handlers == nil then
lib.event_to_handlers = {}
end
local IS_TURTLE_WOW = getglobal("TURTLE_WOW_VERSION") ~= nil
---@shape LcpProfessionProps
---@field icon string
---@type table<string, LcpProfessionProps>
local P = {}
P["Alchemy"] = {icon = [[Interface\Icons\trade_alchemy]]}
P["Blacksmithing"] = {icon = [[Interface\Icons\trade_blacksmithing]]}
P["Cooking"] = {icon = [[Interface\Icons\inv_misc_food_15]]}
P["Enchanting"] = {icon = [[Interface\Icons\trade_engraving]]}
P["Engineering"] = {icon = [[Interface\Icons\trade_engineering]]}
P["First Aid"] = {icon = [[Interface\Icons\spell_holy_sealofsacrifice]]}
P["Leatherworking"] = {icon = [[Interface\Icons\inv_misc_armorkit_17]]}
P["Mining"] = {icon = [[Interface\Icons\spell_fire_flameblades]]}
P["Poisons"] = {icon = [[Interface\Icons\trade_brewpoison]]}
P["Tailoring"] = {icon = [[Interface\Icons\trade_tailoring]]}
if IS_TURTLE_WOW then
P["Jewelcrafting"] = {icon = [[Interface\Icons\inv_helmet_44]]}
P["Survival"] = {icon = [[Interface\Icons\INV_Misc_Rope_01]]}
end
local PROFESSION_TO_PROPS = P
P = {}
---@type table<string, string>
local ENGLISH_TO_LOCALIZED = {}
---@type table<string, string>
local LOCALIZED_TO_ENGLISH = {}
---@shape LcpProfession
---@field english_name string
---@field localized_name string
---@field icon_texture_path string
---@shape LcpKnownProfession: LcpProfession
---@field cur_rank number
---@alias LcpProfessionFrameType "VanillaCraftFrame" | "VanillaTradeSkillFrame" | "TurtleTradeSkillFrame" | "Artisan" | "AdvancedTradeSkillWindow" | "AdvancedTradeSkillWindow2"
---@type {FrameType: table<LcpProfessionFrameType, LcpProfessionFrameType>}
LibCraftingProfessionsConstants = {
FrameType = {
VanillaCraftFrame = "VanillaCraftFrame",
VanillaTradeSkillFrame = "VanillaTradeSkillFrame",
TurtleTradeSkillFrame = "TurtleTradeSkillFrame",
Artisan = "Artisan",
AdvancedTradeSkillWindow = "AdvancedTradeSkillWindow",
AdvancedTradeSkillWindow2 = "AdvancedTradeSkillWindow2",
}
}
local FrameType = LibCraftingProfessionsConstants.FrameType
---@shape LcpProfessionFrame
---@field frame Frame
---@field type LcpProfessionFrameType
---@alias LcpSkillDifficulty "trivial" | "easy" | "medium" | "optimal" | "difficult"
---@shape LcpKnownSkill
---@field localized_name string
---@field item_link string
---@field difficulty LcpSkillDifficulty
---@field num_available number
---@alias LcpScanSkillsHandler fun(profession: LcpKnownProfession, skills: LcpKnownSkill[]):void
---@type table<string, LcpKnownSkill[]>
local skills_by_english_profession_name = {}
---@return boolean
local function ready(value)
if value == nil then
return false
end
if type(value) == "table" then
return next(value) ~= nil
end
return value ~= "" and value ~= 0
end
---
--- Retrieves a list of all supported crafting professions in the game.
---
---@return LcpProfession[]
function lib:GetSupportedProfessions()
---@type LcpProfession[]
local professions = {}
for english_name, props in pairs(PROFESSION_TO_PROPS) do
tinsert(professions, {
english_name = english_name,
localized_name = ENGLISH_TO_LOCALIZED[english_name],
icon_texture_path = props.icon,
})
end
return professions
end
---
--- Retrieves a list of the player's known crafting professions.
--- Returns a table of known professions, or nil if game data is not yet available.
--- Use RegisterEvent("LCP_SKILLS_UPDATE", ...) to ensure data availability.
---
---@return LcpKnownProfession[]|nil
function lib:GetPlayerProfessions()
local num_of_professions = GetNumSkillLines()
if not ready(num_of_professions) then
return nil
end
---@type LcpKnownProfession[]
local professions = {}
for i = 1, num_of_professions do
local localized_name, is_header, _, rank, _, _, _, _, _, _, _, _, _ = GetSkillLineInfo(i)
if not is_header then
if not ready(localized_name) then
return nil
end
local english_name = LOCALIZED_TO_ENGLISH[localized_name]
local crafting_props = PROFESSION_TO_PROPS[english_name]
if crafting_props ~= nil then
if not ready(rank) then
return nil
end
tinsert(professions, {
english_name = english_name,
localized_name = localized_name,
icon_texture_path = crafting_props.icon,
cur_rank = rank,
})
end
end
end
return professions
end
---
--- Retrieves a list of the player's skills for a specific profession.
--- Returns a table of known skills for the specified profession, or nil if game data is not yet available
--- Use RegisterEvent("LCP_SKILLS_UPDATE", ...) to ensure data availability.
---
--- @param profession_name string
--- @return LcpKnownSkill[]|nil
function lib:GetPlayerProfessionSkills(profession_name)
return skills_by_english_profession_name[profession_name] or skills_by_english_profession_name[LOCALIZED_TO_ENGLISH[profession_name]]
end
---
--- Registers a handler for the specific library event.
--- LCP_SKILLS_UPDATE is fired when skill data is updated and ready for retrieval.
--- LCP_FRAME_SHOW is fired when a crafting profession frame is shown.
--- LCP_FRAME_CLOSE is fired when a crafting profession frame is closed.
---@param event string
---@param handler function
---@overload fun(event: "LCP_SKILLS_UPDATE", handler: fun(profession: LcpKnownProfession, skills: LcpKnownSkill[]):void)
---@overload fun(event: "LCP_FRAME_SHOW", handler: fun(profession: LcpKnownProfession, frame: Frame, frame_type: LcpProfessionFrameType):void)
---@overload fun(event: "LCP_FRAME_CLOSE", handler: fun(frame: Frame, frame_type: LcpProfessionFrameType):void)
function lib:RegisterEvent(event, handler)
if lib.event_to_handlers[event] == nil then
lib.event_to_handlers[event] = {}
end
tinsert(lib.event_to_handlers[event], --[[---@type function]] handler)
end
---@shape _LcpFilterOption
---@field name string
---@field type string
---@shape _LcpProfessionAdapter
---@field GetProfessionInfo fun():string|nil, string|nil, number|nil, number|nil
---@field GetNumSkills fun():number|nil
---@field GetSkillInfo fun(index:number):string|nil, string|nil, number|nil, wowboolean
---@field GetSkillItemLink fun(index:number):string|nil
---@field ExpandHeader fun(index:number):void
---@field CollapseHeader fun(index:number):void
---@field DisableFilters fun():_LcpFilterOption[]
---@field EnableFilters fun(options:_LcpFilterOption[]):void
---@param adapter _LcpProfessionAdapter
---@return LcpKnownSkill[]|nil
local function scan_skills(adapter)
local localized_profession, english_profession, cur_rank, max_rank = adapter.GetProfessionInfo()
if not ready(localized_profession) or not ready(english_profession) or not ready(cur_rank) or not ready(max_rank) then
return nil
end
local disabled_filters = adapter.DisableFilters()
local num_of_skills_before_expansion = adapter.GetNumSkills()
if not ready(num_of_skills_before_expansion) then
return nil
end
---@type table<string, true>
local set_of_headers_to_collapse = {}
for i = 1, num_of_skills_before_expansion do
local skill_name, skill_type_or_difficulty, _, is_expanded = adapter.GetSkillInfo(i)
if not ready(skill_name) or not ready(skill_type_or_difficulty) then
return nil
end
if skill_type_or_difficulty == "header" and is_expanded == nil then
set_of_headers_to_collapse[ --[[---@not nil]] skill_name] = true
end
end
adapter.ExpandHeader(0)
local num_of_skills_after_expansion = adapter.GetNumSkills()
if not ready(num_of_skills_after_expansion) then
return nil
end
---@type LcpKnownSkill[]
local skills = {}
for i = 1, num_of_skills_after_expansion do
local skill_name, skill_type_or_difficulty, num_available, _ = adapter.GetSkillInfo(i)
if not ready(skill_name) or not ready(skill_type_or_difficulty) then
return nil
end
if skill_type_or_difficulty ~= "header" then
local item_link = adapter.GetSkillItemLink(i)
if num_available == nil or not ready(item_link) then
return nil
end
---@type LcpKnownSkill
local skill = {
localized_name = --[[---@not nil]] skill_name,
item_link = --[[---@not nil]] item_link,
difficulty = --[[---@type LcpSkillDifficulty]] skill_type_or_difficulty,
num_available = --[[---@not nil]] num_available,
}
tinsert(skills, skill)
end
end
for i = adapter.GetNumSkills(), 1, -1 do
local skill_name, skill_type_or_difficulty, _, _ = adapter.GetSkillInfo(i)
if skill_type_or_difficulty == "header" and set_of_headers_to_collapse[ --[[---@not nil]] skill_name] ~= nil then
adapter.CollapseHeader(i)
end
end
adapter.EnableFilters(disabled_filters)
return skills
end
---@param known_professions LcpKnownProfession[]|nil
local function forget_obsolete_professions(known_professions)
if known_professions == nil then
return
end
---@type table<string, boolean>
local known_profession_names_set = {}
for _, profession in ipairs(--[[---@not nil]] known_professions) do
known_profession_names_set[profession.english_name] = true
end
for english_name, _ in pairs(skills_by_english_profession_name) do
if known_profession_names_set[english_name] == nil then
skills_by_english_profession_name[english_name] = nil
end
end
end
---@param known_professions LcpKnownProfession[]|nil
---@param name string
---@return LcpKnownProfession|nil
local function find_known_profession(known_professions, name)
local profession
for _, candidate in ipairs(known_professions or {}) do
if candidate.localized_name == name or candidate.english_name == name then
profession = candidate
break
end
end
return profession
end
---@param english_name string
---@param skills LcpKnownSkill[]
local function save_skills(english_name, skills)
skills_by_english_profession_name[english_name] = skills
local known_professions = lib:GetPlayerProfessions()
forget_obsolete_professions(known_professions)
local profession = find_known_profession(known_professions, english_name)
if profession == nil then
return
end
for _, handler in ipairs(lib.event_to_handlers["LCP_SKILLS_UPDATE"] or {}) do
handler(profession, skills)
end
end
---@shape LcpThirdPartyProfessionFrameAddon
---@field name string
---@field frame_name string
---@field opens_and_closes_vanilla_profession_frames boolean
---@type LcpThirdPartyProfessionFrameAddon
local artisan = {name = "Artisan", frame_name = "ArtisanFrame", opens_and_closes_vanilla_profession_frames = true}
---@type LcpThirdPartyProfessionFrameAddon
local atsw = {name = "AdvancedTradeSkillWindow", frame_name = "ATSWFrame", opens_and_closes_vanilla_profession_frames = false}
---@type LcpThirdPartyProfessionFrameAddon
local atsw2 = {name = "AdvancedTradeSkillWindow2", frame_name = "ATSWFrame", opens_and_closes_vanilla_profession_frames = true}
local SUPPORTED_THIRD_PARTY_ADDONS = {artisan, atsw, atsw2}
---@param addon LcpThirdPartyProfessionFrameAddon
local function is_third_party_loaded(addon)
return IsAddOnLoaded(addon.name) == 1 and getglobal(addon.frame_name) ~= nil
end
---@return LcpProfessionFrame
local function detect_profession_frame(professionFrame)
---@type Frame
local atsw1_or_atsw2_frame = getglobal(atsw2.frame_name)
if atsw1_or_atsw2_frame ~= nil then
---@type LcpProfessionFrameType
local frame_type = FrameType.AdvancedTradeSkillWindow
if is_third_party_loaded(atsw2) then
frame_type = FrameType.AdvancedTradeSkillWindow2
end
return {frame = atsw1_or_atsw2_frame, type = frame_type}
end
if is_third_party_loaded(artisan) then
return {frame = getglobal(artisan.frame_name), type = FrameType.Artisan}
end
if professionFrame == TradeSkillFrame then
local frame_type = IS_TURTLE_WOW and FrameType.TurtleTradeSkillFrame or FrameType.VanillaTradeSkillFrame
return {frame = professionFrame, type = frame_type}
end
return {frame = professionFrame, type = FrameType.VanillaCraftFrame}
end
---@param english_name string
---@param localized_name string
---@param cur_rank number
---@param frame Frame
local function send_frame_show_event(english_name, localized_name, cur_rank, frame)
local profession = {
english_name = english_name,
localized_name = localized_name,
cur_rank = cur_rank,
}
local real_frame = detect_profession_frame(frame)
for _, handler in ipairs(lib.event_to_handlers["LCP_FRAME_SHOW"] or {}) do
handler(profession, real_frame.frame, real_frame.type)
end
end
---@param frame Frame
local function send_frame_close_event(frame)
local real_frame = detect_profession_frame(frame)
for _, handler in ipairs(lib.event_to_handlers["LCP_FRAME_CLOSE"] or {}) do
handler(real_frame.frame, real_frame.type)
end
end
---@return CraftFrame
local function get_craft_frame()
if CraftFrame == nil then
LoadAddOn("Blizzard_CraftUI")
end
return CraftFrame
end
---@type _LcpProfessionAdapter
local craft_adapter = {
GetProfessionInfo = function()
local name, cur_rank, max_rank = GetCraftDisplaySkillLine()
if name == nil then -- GetCraftDisplaySkillLine() returns nil for the "Beast Training" frame
name = GetCraftName()
end
return name, LOCALIZED_TO_ENGLISH[--[[---@type string]] name], cur_rank, max_rank
end,
GetNumSkills = GetNumCrafts,
GetSkillInfo = function(i)
local name, _, type, num_available, is_expanded = GetCraftInfo(i)
return name, type, num_available, is_expanded
end,
GetSkillItemLink = GetCraftItemLink,
ExpandHeader = ExpandCraftSkillLine,
CollapseHeader = CollapseCraftSkillLine,
DisableFilters = function() return {} end,
EnableFilters = function(options) end,
}
local function try_send_craft_frame_show_event()
local localized_name, english_name, cur_rank, _ = craft_adapter.GetProfessionInfo()
if not ready(localized_name) or not ready(english_name) or not ready(cur_rank) then
return
end
send_frame_show_event(--[[---@not nil]] english_name, --[[---@not nil]] localized_name, --[[---@not nil]] cur_rank, get_craft_frame())
end
local craft_scan_in_progress = false
local function scan_craft_frame()
craft_scan_in_progress = true
local localized_name, english_name, _, _ = craft_adapter.GetProfessionInfo()
if not ready(localized_name) or not ready(english_name) then
craft_scan_in_progress = false
return
end
local skills = scan_skills(craft_adapter)
if not ready(skills) then
craft_scan_in_progress = false
return
end
save_skills(--[[---@not nil]] english_name, --[[---@not nil]] skills)
craft_scan_in_progress = false
end
---@shape _LcpSkillFilterAdapter
---@field GetType fun(): "inv_slot" | "subclass"
---@field GetOptions fun(): string...
---@field GetOptionState fun(index:number): wowboolean
---@field SetOptionState fun(index:number, enable:wowboolean, exclusive:wowboolean): void
---@param adapter _LcpSkillFilterAdapter
---@return _LcpFilterOption[]
local function disable_trade_skill_filters(adapter)
if adapter.GetOptionState(0) == 1 then
return {}
end
---@type _LcpFilterOption[]
local selected_options = {}
for i, option in ipairs({adapter.GetOptions()}) do
if adapter.GetOptionState(i) == 1 then
tinsert(selected_options, {name = option, type = adapter.GetType()})
end
end
adapter.SetOptionState(0, 1, 1)
return selected_options
end
---@param adapter _LcpSkillFilterAdapter
---@param options _LcpFilterOption[]
local function enable_trade_skill_filters(adapter, options)
---@type table<string, boolean>
local options_set = {}
for _, option in ipairs(options) do
if option.type == adapter.GetType() then
options_set[option.name] = true
end
end
for i, option in ipairs({adapter.GetOptions()}) do
if options_set[option] then
adapter.SetOptionState(i, 1, 1)
end
end
end
local function get_trade_skill_frame()
if TradeSkillFrame == nil then
LoadAddOn("Blizzard_TradeSkillUI")
end
return TradeSkillFrame
end
---@type _LcpSkillFilterAdapter
local inv_slot_filter_adapter = {
GetType = function() return "inv_slot" end,
GetOptions = GetTradeSkillInvSlots,
GetOptionState = GetTradeSkillInvSlotFilter,
SetOptionState = SetTradeSkillInvSlotFilter,
}
---@type _LcpSkillFilterAdapter
local subclass_filter_adapter = {
GetType = function() return "subclass" end,
GetOptions = GetTradeSkillSubClasses,
GetOptionState = GetTradeSkillSubClassFilter,
SetOptionState = SetTradeSkillSubClassFilter,
}
---@type _LcpProfessionAdapter
local trade_skill_adapter = {
GetProfessionInfo = function()
local name, cur_rank, max_rank = GetTradeSkillLine()
return name, LOCALIZED_TO_ENGLISH[--[[---@type string]] name], cur_rank, max_rank
end,
GetNumSkills = GetNumTradeSkills,
GetSkillInfo = GetTradeSkillInfo,
GetSkillItemLink = GetTradeSkillItemLink,
ExpandHeader = ExpandTradeSkillSubClass,
CollapseHeader = CollapseTradeSkillSubClass,
DisableFilters = function()
local result = {}
for _, adapter in ipairs({inv_slot_filter_adapter, subclass_filter_adapter}) do
for _, option in ipairs(disable_trade_skill_filters(adapter)) do
tinsert(result, option)
end
end
return result
end,
EnableFilters = function(options)
for _, adapter in ipairs({inv_slot_filter_adapter, subclass_filter_adapter}) do
enable_trade_skill_filters(adapter, options)
end
end,
}
local function try_send_trade_skill_frame_show_event()
local localized_name, english_name, cur_rank, _ = trade_skill_adapter.GetProfessionInfo()
if not ready(localized_name) or not ready(english_name) or not ready(cur_rank) then
return false
end
send_frame_show_event(--[[---@not nil]] english_name, --[[---@not nil]] localized_name, --[[---@not nil]] cur_rank, get_trade_skill_frame())
end
local trade_skill_scan_in_progress = false
---@return boolean
local function scan_trade_skill_frame()
trade_skill_scan_in_progress = true
local localized_name, english_name, _, _ = trade_skill_adapter.GetProfessionInfo()
if not ready(localized_name) or not ready(english_name) then
trade_skill_scan_in_progress = false
return false
end
local skills = scan_skills(trade_skill_adapter)
if not ready(skills) then
trade_skill_scan_in_progress = false
return false
end
save_skills(--[[---@not nil]] english_name, --[[---@not nil]] skills)
trade_skill_scan_in_progress = false
return true
end
local L = ENGLISH_TO_LOCALIZED
local game_locale = GetLocale()
if game_locale == "enUS" or game_locale == "enGB" then
L["Alchemy"] = "Alchemy"
L["Blacksmithing"] = "Blacksmithing"
L["Cooking"] = "Cooking"
L["Enchanting"] = "Enchanting"
L["Engineering"] = "Engineering"
L["First Aid"] = "First Aid"
L["Leatherworking"] = "Leatherworking"
L["Mining"] = "Mining"
L["Poisons"] = "Poisons"
L["Tailoring"] = "Tailoring"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Jewelcrafting"
L["Survival"] = "Survival"
end
elseif game_locale == "deDE" then
L["Alchemy"] = "Alchimie"
L["Blacksmithing"] = "Schmiedekunst"
L["Cooking"] = "Kochkunst"
L["Enchanting"] = "Verzauberkunst"
L["Engineering"] = "Ingenieurskunst"
L["First Aid"] = "Erste Hilfe"
L["Leatherworking"] = "Lederverarbeitung"
L["Mining"] = "Bergbau"
L["Poisons"] = "Gifte"
L["Tailoring"] = "Schneiderei"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Juwelenschleifen"
L["Survival"] = "Survival"
end
elseif game_locale == "esES" then
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Herrería"
L["Cooking"] = "Cocina"
L["Enchanting"] = "Encantamiento"
L["Engineering"] = "Ingeniería"
L["First Aid"] = "Primeros auxilios"
L["Leatherworking"] = "Peletería"
L["Mining"] = "Minería"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Sastrería"
if IS_TURTLE_WOW then
L["Blacksmithing"] = "Ferraria"
L["Cooking"] = "Culinária"
L["Enchanting"] = "Encantamento"
L["Engineering"] = "Engenharia"
L["First Aid"] = "Primeiros Socorros"
L["Jewelcrafting"] = "Joalheria"
L["Survival"] = "Survival"
L["Leatherworking"] = "Couraria"
L["Mining"] = "Mineração"
L["Tailoring"] = "Alfaiataria"
end
elseif game_locale == "esMX" then
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Herrería"
L["Cooking"] = "Cocina"
L["Enchanting"] = "Encantamiento"
L["Engineering"] = "Ingeniería"
L["First Aid"] = "Primeros auxilios"
L["Leatherworking"] = "Peletería"
L["Mining"] = "Minería"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Sastrería"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Joyería"
L["Survival"] = "Survival"
end
elseif game_locale == "frFR" then
L["Alchemy"] = "Alchimie"
L["Blacksmithing"] = "Forge"
L["Cooking"] = "Cuisine"
L["Enchanting"] = "Enchantement"
L["Engineering"] = "Ingénieur"
L["First Aid"] = "Premiers soins"
L["Leatherworking"] = "Travail du cuir"
L["Mining"] = "Minage"
L["Poisons"] = "Poisons"
L["Tailoring"] = "Couture"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Joaillerie"
L["Survival"] = "Survival"
end
elseif game_locale == "koKR" then
L["Alchemy"] = "연금술"
L["Blacksmithing"] = "대장기술"
L["Cooking"] = "요리"
L["Enchanting"] = "마법부여"
L["Engineering"] = "기계공학"
L["First Aid"] = "응급치료"
L["Leatherworking"] = "가죽세공"
L["Mining"] = "채광"
L["Poisons"] = "독 조제"
L["Tailoring"] = "재봉술"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "보석세공"
L["Survival"] = "Survival"
end
elseif game_locale == "ptBR" then
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Ferraria"
L["Cooking"] = "Culinária"
L["Enchanting"] = "Encantamento"
L["Engineering"] = "Engenharia"
L["First Aid"] = "Primeiros Socorros"
L["Leatherworking"] = "Couraria"
L["Mining"] = "Mineração"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Alfaiataria"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Joalheria"
L["Survival"] = "Survival"
end
elseif game_locale == "ruRU" then
L["Alchemy"] = "Алхимия"
L["Blacksmithing"] = "Кузнечное дело"
L["Cooking"] = "Кулинария"
L["Enchanting"] = "Наложение чар"
L["Engineering"] = "Инженерное дело"
L["First Aid"] = "Первая помощь"
L["Leatherworking"] = "Кожевничество"
L["Mining"] = "Горное дело"
L["Poisons"] = "Яды"
L["Tailoring"] = "Портняжное дело"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "Ювелирное дело"
L["Survival"] = "Survival"
end
elseif game_locale == "zhCN" then
L["Alchemy"] = "炼金术"
L["Blacksmithing"] = "锻造"
L["Cooking"] = "烹饪"
L["Enchanting"] = "附魔"
L["Engineering"] = "工程学"
L["First Aid"] = "急救"
L["Leatherworking"] = "制皮"
L["Mining"] = "采矿"
L["Poisons"] = "毒药"
L["Tailoring"] = "裁缝"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "珠宝加工"
L["Survival"] = "Survival"
end
elseif game_locale == "zhTW" then
L["Alchemy"] = "煉金術"
L["Blacksmithing"] = "鍛造"
L["Cooking"] = "烹飪"
L["Enchanting"] = "附魔"
L["Engineering"] = "工程學"
L["First Aid"] = "急救"
L["Leatherworking"] = "製皮"
L["Mining"] = "採礦"
L["Poisons"] = "毒藥"
L["Tailoring"] = "裁縫"
if IS_TURTLE_WOW then
L["Jewelcrafting"] = "珠寶設計"
L["Survival"] = "Survival"
end
end
for english_name, localized_name in pairs(ENGLISH_TO_LOCALIZED) do
LOCALIZED_TO_ENGLISH[localized_name] = english_name
end
---@param frame Frame
---@return table
local function as_table(frame)
return --[[---@type table]] frame
end
local was_third_party_on_hide_hooked = false
---@param addon LcpThirdPartyProfessionFrameAddon
local function hook_third_party_on_hide(addon)
if not is_third_party_loaded(addon) then
return
end
---@type Frame
local frame = getglobal(addon.frame_name)
if frame == nil then
return
end
if as_table(frame).is_hooked_by_lcp == true then
was_third_party_on_hide_hooked = true
return
end
local on_hide = frame:GetScript("OnHide")
frame:SetScript("OnHide", function()
if on_hide ~= nil then
(--[[---@not nil]] on_hide)()
end
send_frame_close_event(frame)
end)
as_table(frame).is_hooked_by_lcp = true
was_third_party_on_hide_hooked = true
end
if lib.event_frame ~= nil then
lib.event_frame:UnregisterAllEvents()
lib.event_frame:SetScript("OnUpdate", nil)
lib.event_frame:SetScript("OnEvent", nil)
end
local render_pseudo_time = 0
local craft_frame_successfully_scanned_at = 0
local trade_skill_frame_successfully_scanned_at = 0
lib.event_frame = CreateFrame("Frame")
lib.event_frame:SetScript("OnUpdate", function ()
-- Third-party addons can trigger multiple UPDATE events
-- so we count rendered frames to not scan skills more than once per frame
render_pseudo_time = render_pseudo_time + 1
if render_pseudo_time >= 100 then
render_pseudo_time = 1
end
end)
lib.event_frame:SetScript("OnEvent", function()
local do_craft_scan = false
local do_trade_skill_scan = false
if event == "ADDON_LOADED" then
for _, addon in ipairs(SUPPORTED_THIRD_PARTY_ADDONS) do
if addon.name == arg1 and addon.opens_and_closes_vanilla_profession_frames then
hook_third_party_on_hide(addon)
end
end
elseif event == "CRAFT_SHOW" then
try_send_craft_frame_show_event()
do_craft_scan = true
elseif event == "CRAFT_UPDATE" then
if not craft_scan_in_progress then
-- Learned new skill AND craft frame is opened
do_craft_scan = true
end
elseif event == "CRAFT_CLOSE" then
if not was_third_party_on_hide_hooked then
send_frame_close_event(get_craft_frame())
end
elseif event == "TRADE_SKILL_SHOW" then
try_send_trade_skill_frame_show_event()
do_trade_skill_scan = true
elseif event == "TRADE_SKILL_UPDATE" then
if not trade_skill_scan_in_progress then
-- Learned new skill AND trade skill frame is opened
do_trade_skill_scan = true
end
elseif event == "TRADE_SKILL_CLOSE" then
if not was_third_party_on_hide_hooked then
send_frame_close_event(get_trade_skill_frame())
end
elseif event == "CHARACTER_POINTS_CHANGED" then
-- Learned or unlearned profession/specialization
forget_obsolete_professions(lib:GetPlayerProfessions())
elseif event == "SKILL_LINES_CHANGED" then
-- Raised the profession skill level OR learned/unlearned profession/specialization
do_craft_scan = true
do_trade_skill_scan = true
end
if do_craft_scan and craft_frame_successfully_scanned_at ~= render_pseudo_time then
if scan_craft_frame() then
craft_frame_successfully_scanned_at = render_pseudo_time
end
end
if do_trade_skill_scan and trade_skill_frame_successfully_scanned_at ~= render_pseudo_time then
if scan_trade_skill_frame() then
trade_skill_frame_successfully_scanned_at = render_pseudo_time
end
end
end)
lib.event_frame:RegisterEvent("ADDON_LOADED")
lib.event_frame:RegisterEvent("CRAFT_SHOW")
lib.event_frame:RegisterEvent("CRAFT_UPDATE")
lib.event_frame:RegisterEvent("CRAFT_CLOSE")
lib.event_frame:RegisterEvent("TRADE_SKILL_SHOW")
lib.event_frame:RegisterEvent("TRADE_SKILL_UPDATE")
lib.event_frame:RegisterEvent("TRADE_SKILL_CLOSE")
lib.event_frame:RegisterEvent("CHARACTER_POINTS_CHANGED")
lib.event_frame:RegisterEvent("SKILL_LINES_CHANGED")
for _, addon in ipairs(SUPPORTED_THIRD_PARTY_ADDONS) do
if is_third_party_loaded(addon) and addon.opens_and_closes_vanilla_profession_frames then
hook_third_party_on_hide(addon)
end
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="LibStub.lua" />
<Script file="LibCraftingProfessions-1.0.lua" />
</Ui>
@@ -0,0 +1,63 @@
### Revision 14 (2025/12/21)
- Turtle: Update localization
### Revision 14 (2025/09/09)
- Turtle: Add a lot of missing recipes
- Fix a bunch of data errors
- Add craft spell id to craft objects
- Fix debug mode
### Revision 13 (2025/09/05)
- Turtle: Add new 1.18 alchemy concoction recipes
- Turtle: Add localization updates from patch 1.18.7230
### Revision 13 (2025/08/17)
- Turtle: Support the latest localization updates (including deDE game client)
### Revision 12 (2025/08/16)
- Turtle: Fix Jewelcrafting database errors
- Turtle: Add some missing 1.17.2 recipes
- Turtle: Add some 1.18.0 recipe changes
- Turtle: Support Disguise profession
### Revision 11 (2025/06/11)
- Fix a lot of database errors
### Revision 10 (2025/06/10)
- Fix localized profession names
### Revision 9 (2025/06/10)
- Vanilla: Fix a bunch of localization mistakes
- Turtle: Fully support esES, ptBR and zhCN locales
- Turtle: Add several missing crafts
- Turtle: Fix and rework Jewelcrafting
### Revision 8 (2025/06/07)
- Attempt to work around a supposed bug in SpellInfo SuperWoW function
### Revision 7 (2025/06/04)
- Significantly improve localization support by using SuperWoW (if available)
### Revision 6 (2025/06/01)
- Add a couple of missing Turtle-specific recipes
- Add GetCraftsByRecipeId to public API
### Revision 5 (2025/05/03)
- Support Turtle WoW Jewelcrafting profession (by [KasVital](https://github.com/KasVital))
### Revision 4 (2024/11/09)
- Add locale modules for profession names
- Add GetCraftsByProfession() API
- Fix First Aid profession name
- Fix library in-memory upgrade process
### Revision 3 (2024/11/03)
- Add new Turtle WoW recipes for Vanilla professions
- Fix craft indentation
- Update some translations
### Revision 2 (2024/11/02)
- Disable debug mode
### Revision 1 (2024/11/02)
- Release the first version
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Roman Kharitonov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,482 @@
--[[
Name: LibCrafts-1.0
Developed by: Refaim (rkharito@yandex.ru, https://github.com/refaim/)
Website: https://github.com/refaim/LibCrafts-1.0
Description: A library designed to provide a database crafting spells, recipes, reagents etc.
Dependencies: LibStub
Compatibility: Vanilla (1.12.1), Turtle (1.18.0)
]]
---@type LibStubDef
local LibStub = getglobal("LibStub")
assert(LibStub ~= nil)
local untyped_lib, _ = LibStub:NewLibrary("LibCrafts-1.0", 20)
if not untyped_lib then return end
---@class LibCrafts
---@field env LcEnvironment
---@field constants LcConstants
---@field modules_by_name table<string, LcModule>
---@field spell_id_to_craft table<number, LcCraft>
---@field reagent_id_to_spell_ids_set table<number, table<number, boolean>>
---@field recipe_id_to_spell_ids_set table<number, table<number, boolean>>
---@field localized_profession_name_to_spell_ids_set table<string, table<number, boolean>>
local lib = --[[---@type LibCrafts]] untyped_lib
lib.modules_by_name = lib.modules_by_name or {}
lib.spell_id_to_craft = lib.spell_id_to_craft or {}
lib.reagent_id_to_spell_ids_set = lib.reagent_id_to_spell_ids_set or {}
lib.recipe_id_to_spell_ids_set = lib.recipe_id_to_spell_ids_set or {}
lib.localized_profession_name_to_spell_ids_set = lib.localized_profession_name_to_spell_ids_set or {}
---@type table[]
local data_tables = {
lib.modules_by_name,
lib.spell_id_to_craft,
lib.reagent_id_to_spell_ids_set,
lib.recipe_id_to_spell_ids_set,
lib.localized_profession_name_to_spell_ids_set,
}
local all_data_tables_filled = true
for _, data_table in ipairs(data_tables) do
if type(data_table) ~= "table" or next(data_table) == nil then
all_data_tables_filled = false
break
end
end
if not all_data_tables_filled then
lib.modules_by_name = {}
lib.spell_id_to_craft = {}
lib.reagent_id_to_spell_ids_set = {}
lib.recipe_id_to_spell_ids_set = {}
lib.localized_profession_name_to_spell_ids_set = {}
end
lib.env = {
is_debug = false,
is_turtle_wow = getglobal("TURTLE_WOW_VERSION") ~= nil,
is_super_wow_loaded = getglobal("SpellInfo") ~= nil,
}
lib.constants = {
qualities = {
Poor = 101,
Common = 102,
Uncommon = 103,
Rare = 104,
Epic = 105,
Legendary = 106,
},
spell_sources = {
LearnedAutomatically = 201,
Quest = 202,
Trainer = 203,
WorldObject = 204,
},
recipe_sources = {
Chest = 301,
CraftedByEngineer = 302,
Drop = 303,
Fishing = 304,
GiftedToReturningEngineers = 305,
Pickpocketing = 306,
Quest = 307,
Vendor = 308,
},
}
---@shape LcEnvironment
---@field is_debug boolean
---@field is_turtle_wow boolean
---@field is_super_wow_loaded boolean
---@shape LcConstants
---@field qualities LcItemQualities
---@field spell_sources LcSpellSources
---@field recipe_sources LcRecipeSources
---@shape LcItemQualities
---@field Poor 101
---@field Common 102
---@field Uncommon 103
---@field Rare 104
---@field Epic 105
---@field Legendary 106
---@alias LcItemQuality 101 | 102 | 103 | 104 | 105 | 106
---@shape LcSpellSources
---@field LearnedAutomatically 201
---@field Quest 202
---@field Trainer 203
---@field WorldObject 204
---@alias LcSpellSource 201 | 202 | 203 | 204
---@shape LcRecipeSources
---@field Chest 301
---@field CraftedByEngineer 302
---@field Drop 303
---@field Fishing 304
---@field GiftedToReturningEngineers 305
---@field Pickpocketing 306
---@field Quest 307
---@field Vendor 308
---@alias LcRecipeSource 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308
---@shape LcItem
---@field id number
---@shape LcRecipe: LcItem
---@field quality LcItemQuality
---@field sources LcRecipeSource[]
---@class LcCraft
---@field spell_id number
---@field en_profession_name string
---@field localized_profession_name string
---@field spell_id number
---@field localized_spell_name string
---@field skill_level number
---@field character_level number
---@field result LcItem|nil
---@field sources LcSpellSource[]
---@field recipes LcRecipe[]
---@field reagent_id_to_count table<number, number>
---@field was_enriched boolean
local Craft = {}
---@param value string
---@param locale_module_name string
---@return string
local function translate_from_en_to_game_locale(value, locale_module_name)
---@type table<string, string|boolean>
local L = {}
local locale_module = --[[---@type LcLocaleModule]] lib.modules_by_name[locale_module_name]
if locale_module ~= nil then
L = locale_module:GetStrings()
end
local localized_value = L[value]
if type(localized_value) ~= "string" or localized_value == "" then
localized_value = value
end
return --[[---@type string]] localized_value
end
---@param craft LcCraft
---@return LcCraft
local function enrich(craft)
if not lib.env.is_super_wow_loaded or craft.was_enriched then
return craft
end
local spell_name, _, _, _, _ = SpellInfo(craft.spell_id)
if type(spell_name) == "string" then
craft.localized_spell_name = spell_name
craft.was_enriched = true
end
return craft
end
---
--- Returns a list of crafts by reagent item id.
---
---@param item_id number
---@return LcCraft[]
function lib:GetCraftsByReagentId(item_id)
local crafts = {}
for spell_id, _ in pairs(self.reagent_id_to_spell_ids_set[item_id] or {}) do
tinsert(crafts, enrich(self.spell_id_to_craft[spell_id]))
end
return crafts
end
---
--- Returns a list of crafts by recipe item id.
---
---@param item_id number
---@return LcCraft[]
function lib:GetCraftsByRecipeId(item_id)
local crafts = {}
for spell_id, _ in pairs(self.recipe_id_to_spell_ids_set[item_id] or {}) do
tinsert(crafts, enrich(self.spell_id_to_craft[spell_id]))
end
return crafts
end
---
--- Returns a list of crafts by profession name in current game locale (or in English).
---
---@param profession string
---@return LcCraft[]
function lib:GetCraftsByProfession(profession)
local d = self.localized_profession_name_to_spell_ids_set
local spell_ids_set = d[profession] or d[translate_from_en_to_game_locale(profession, "Locales-Professions")] or {}
local crafts = {}
for spell_id, _ in pairs(spell_ids_set) do
tinsert(crafts, enrich(self.spell_id_to_craft[spell_id]))
end
return crafts
end
---@class LcModule
---@field name string
---@field version number
---@class LcProfessionModule: LcModule
---@field en_profession_name string
---@field localized_profession_name string
local ProfessionModule = {}
---@class LcLocaleModule: LcModule
---@field en_to_any table<string, string|boolean>
local LocaleModule = {}
---@return table<string, string|boolean>
function LocaleModule:GetStrings()
return self.en_to_any
end
---@param name string
---@param version number
---@return boolean
local function module_registered(name, version)
local current_version = (lib.modules_by_name[name] or {}).version
if current_version ~= nil and current_version >= version then
return true
end
return false
end
---@param name string
---@param version number
---@param en_profession_name string
---@return LcProfessionModule|nil
function lib:RegisterProfessionModule(name, version, en_profession_name)
if self.env.is_debug then
assert(type(name) == "string" and name ~= "")
assert(type(version) == "number" and version > 0)
assert(type(en_profession_name) == "string" and en_profession_name ~= "")
end
if module_registered(name, version) then
return nil
end
local object = {}
setmetatable(object, {__index = ProfessionModule})
local module = --[[---@type LcProfessionModule]] object
module.name = name
module.version = version
module.en_profession_name = en_profession_name
module.localized_profession_name = translate_from_en_to_game_locale(en_profession_name, "Locales-Professions")
self.modules_by_name[name] = module
return module
end
---@param name string
---@param locale string
---@param version number
---@return LcLocaleModule|nil
function lib:RegisterLocaleModule(name, locale, version)
if self.env.is_debug then
assert(type(name) == "string" and locale ~= "")
assert(type(locale) == "string" and locale ~= "")
assert(type(version) == "number" and version > 0)
end
if locale == "enGB" then
locale = "enUS"
end
local game_locale = GetLocale()
if locale ~= game_locale or module_registered(name, version) then
return nil
end
local object = {}
setmetatable(object, {__index = LocaleModule})
local module = --[[---@type LcLocaleModule]] object
module.name = name
module.version = version
module.en_to_any = {}
self.modules_by_name[name] = module
return module
end
---@type table<number, boolean>
local SPELL_SOURCE_SET = {}
for _, source in pairs(lib.constants.spell_sources) do
SPELL_SOURCE_SET[source] = true
end
---@param spell_id number
---@param spell_name string
---@param skill_level number
---@param sources LcSpellSource[]
---@return LcCraft
function ProfessionModule:NewCraft(spell_id, spell_name, skill_level, sources)
if lib.env.is_debug then
assert(type(spell_id) == "number" and spell_id > 0)
assert(type(spell_name) == "string" and spell_name ~= "")
assert(type(skill_level) == "number" and skill_level > 0)
assert(type(sources) == "table")
for _, source in ipairs(sources) do
assert(type(source) == "number" and SPELL_SOURCE_SET[source] ~= nil)
end
end
local object = {}
setmetatable(object, {__index = Craft})
local craft = --[[---@type LcCraft]] object
craft.spell_id = spell_id
craft.en_profession_name = self.en_profession_name
craft.localized_profession_name = self.localized_profession_name
craft.spell_id = spell_id
craft.localized_spell_name = translate_from_en_to_game_locale(spell_name, "Locales-Spells")
craft.skill_level = skill_level
craft.character_level = 1
craft.sources = sources
craft.recipes = {}
craft.reagent_id_to_count = {}
return craft
end
---@type table<number, boolean>
local ITEM_QUALITY_SET = {}
for _, quality in pairs(lib.constants.qualities) do
ITEM_QUALITY_SET[quality] = true
end
---@type table<number, boolean>
local RECIPE_SOURCE_SET = {}
for _, source in pairs(lib.constants.recipe_sources) do
RECIPE_SOURCE_SET[source] = true
end
---@param id number
---@param quality LcItemQuality
---@param sources LcRecipeSource[]
---@return self
function Craft:AddRecipe(id, quality, sources)
if lib.env.is_debug then
assert(type(id) == "number" and id > 0)
assert(type(quality) == "number" and ITEM_QUALITY_SET[quality] ~= nil)
assert(type(sources) == "table")
for _, source in ipairs(sources) do
assert(type(source) == "number" and RECIPE_SOURCE_SET[source] ~= nil)
end
for _, recipe in ipairs(self.recipes) do
assert(recipe.id ~= id)
end
end
tinsert(self.recipes, {id = id, quality = quality, sources = sources})
return self
end
---@param item_id number
---@return self
function Craft:SetResult(item_id)
if lib.env.is_debug then
assert(type(item_id) == "number" and item_id > 0)
assert(self.result == nil)
end
self.result = {id = item_id }
return self
end
---@param id number
---@param count number
---@return self
function Craft:AddReagent(id, count)
if lib.env.is_debug then
assert(type(id) == "number" and id > 0)
assert(type(count) == "number" and count > 0)
assert(self.reagent_id_to_count[id] == nil)
end
self.reagent_id_to_count[id] = count
return self
end
---@param level number
---@return self
function Craft:SetMinCharacterLevel(level)
if lib.env.is_debug then
assert(type(level) == "number" and level > 0)
end
self.character_level = level
return self
end
---@generic K
---@param t table<K, table<number, boolean>>
---@param key K
---@return table<number, boolean>
local function get_or_create_set(t, key)
local set = t[key]
if set == nil then
set = {}
t[key] = set
end
return set
end
function Craft:Save()
if lib.env.is_debug then
assert(next(self.reagent_id_to_count) ~= nil)
end
self.was_enriched = false
if lib.env.is_super_wow_loaded then
enrich(self)
end
local old_craft = lib.spell_id_to_craft[self.spell_id]
if old_craft ~= nil then
for reagent_id, _ in pairs(old_craft.reagent_id_to_count) do
lib.reagent_id_to_spell_ids_set[reagent_id][self.spell_id] = nil
end
for _, recipe in ipairs(old_craft.recipes) do
lib.recipe_id_to_spell_ids_set[recipe.id][self.spell_id] = nil
end
lib.localized_profession_name_to_spell_ids_set[old_craft.localized_profession_name][self.spell_id] = nil
end
lib.spell_id_to_craft[self.spell_id] = self
for reagent_id, _ in pairs(self.reagent_id_to_count) do
get_or_create_set(lib.reagent_id_to_spell_ids_set, reagent_id)[self.spell_id] = true
end
for _, recipe in ipairs(self.recipes) do
get_or_create_set(lib.recipe_id_to_spell_ids_set, recipe.id)[self.spell_id] = true
end
get_or_create_set(lib.localized_profession_name_to_spell_ids_set, self.localized_profession_name)[self.spell_id] = true
end
---@return LibCrafts
function LibCraftsGetLibrary()
return lib
end
@@ -0,0 +1,6 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="LibStub.lua" />
<Script file="LibCrafts-1.0.lua" />
<Include file="Locales\Locales.xml"/>
<Include file="Professions\Professions.xml"/>
</Ui>
@@ -0,0 +1,33 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local _G = getfenv()
local strfind, strfmt = string.find, string.format
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or { libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
local _,_,num = strfind(minor, "(%d+)")
minor = assert(tonumber(num), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(strfmt("Cannot find a library instance of %q.", tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
@@ -0,0 +1,22 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="deDE-Professions.lua" />
<Script file="deDE-Spells.lua" />
<Script file="enUS-Professions.lua" />
<Script file="enUS-Spells.lua" />
<Script file="esES-Professions.lua" />
<Script file="esES-Spells.lua" />
<Script file="esMX-Professions.lua" />
<Script file="esMX-Spells.lua" />
<Script file="frFR-Professions.lua" />
<Script file="frFR-Spells.lua" />
<Script file="koKR-Professions.lua" />
<Script file="koKR-Spells.lua" />
<Script file="ptBR-Professions.lua" />
<Script file="ptBR-Spells.lua" />
<Script file="ruRU-Professions.lua" />
<Script file="ruRU-Spells.lua" />
<Script file="zhCN-Professions.lua" />
<Script file="zhCN-Spells.lua" />
<Script file="zhTW-Professions.lua" />
<Script file="zhTW-Spells.lua" />
</Ui>
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "deDE", 5
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Alchimie"
L["Blacksmithing"] = "Schmiedekunst"
L["Cooking"] = "Kochkunst"
L["Enchanting"] = "Verzauberkunst"
L["Engineering"] = "Ingenieurskunst"
L["First Aid"] = "Erste Hilfe"
L["Leatherworking"] = "Lederverarbeitung"
L["Mining"] = "Bergbau"
L["Poisons"] = "Gifte"
L["Tailoring"] = "Schneiderei"
if lib.env.is_turtle_wow then
L["Disguise"] = "Verkleiden"
L["Jewelcrafting"] = "Juwelenschleifen"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "enUS", 5
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = true
L["Blacksmithing"] = true
L["Cooking"] = true
L["Enchanting"] = true
L["Engineering"] = true
L["First Aid"] = true
L["Leatherworking"] = true
L["Mining"] = true
L["Poisons"] = true
L["Tailoring"] = true
if lib.env.is_turtle_wow then
L["Disguise"] = true
L["Jewelcrafting"] = true
L["Survival"] = true
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "esES", 7
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Herrería"
L["Cooking"] = "Cocina"
L["Enchanting"] = "Encantamiento"
L["Engineering"] = "Ingeniería"
L["First Aid"] = "Primeros auxilios"
L["Leatherworking"] = "Peletería"
L["Mining"] = "Minería"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Sastrería"
if lib.env.is_turtle_wow then
L["Blacksmithing"] = "Ferraria"
L["Cooking"] = "Culinária"
L["Disguise"] = "Disfarce"
L["Enchanting"] = "Encantamento"
L["Engineering"] = "Engenharia"
L["First Aid"] = "Primeiros Socorros"
L["Jewelcrafting"] = "Joalheria"
L["Leatherworking"] = "Couraria"
L["Mining"] = "Mineração"
L["Tailoring"] = "Alfaiataria"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "esMX", 4
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Herrería"
L["Cooking"] = "Cocina"
L["Enchanting"] = "Encantamiento"
L["Engineering"] = "Ingeniería"
L["First Aid"] = "Primeros auxilios"
L["Leatherworking"] = "Peletería"
L["Mining"] = "Minería"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Sastrería"
if lib.env.is_turtle_wow then
L["Disguise"] = "Disfarce"
L["Jewelcrafting"] = "Joalheria"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "frFR", 5
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Alchimie"
L["Blacksmithing"] = "Forge"
L["Cooking"] = "Cuisine"
L["Enchanting"] = "Enchantement"
L["Engineering"] = "Ingénieur"
L["First Aid"] = "Premiers soins"
L["Leatherworking"] = "Travail du cuir"
L["Mining"] = "Minage"
L["Poisons"] = "Poisons"
L["Tailoring"] = "Couture"
if lib.env.is_turtle_wow then
L["Disguise"] = "Déguisement"
L["Jewelcrafting"] = "Joaillerie"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "koKR", 5
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "연금술"
L["Blacksmithing"] = "대장기술"
L["Cooking"] = "요리"
L["Enchanting"] = "마법부여"
L["Engineering"] = "기계공학"
L["First Aid"] = "응급치료"
L["Leatherworking"] = "가죽세공"
L["Mining"] = "채광"
L["Poisons"] = "독 조제"
L["Tailoring"] = "재봉술"
if lib.env.is_turtle_wow then
L["Disguise"] = "변장술"
L["Jewelcrafting"] = "보석세공"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "ptBR", 7
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Alquimia"
L["Blacksmithing"] = "Ferraria"
L["Cooking"] = "Culinária"
L["Enchanting"] = "Encantamento"
L["Engineering"] = "Engenharia"
L["First Aid"] = "Primeiros Socorros"
L["Leatherworking"] = "Couraria"
L["Mining"] = "Mineração"
L["Poisons"] = "Venenos"
L["Tailoring"] = "Alfaiataria"
if lib.env.is_turtle_wow then
L["Disguise"] = "Disfarce"
L["Jewelcrafting"] = "Joalheria"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "ruRU", 4
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "Алхимия"
L["Blacksmithing"] = "Кузнечное дело"
L["Cooking"] = "Кулинария"
L["Enchanting"] = "Наложение чар"
L["Engineering"] = "Инженерное дело"
L["First Aid"] = "Первая помощь"
L["Leatherworking"] = "Кожевничество"
L["Mining"] = "Горное дело"
L["Poisons"] = "Яды"
L["Tailoring"] = "Портняжное дело"
if lib.env.is_turtle_wow then
L["Disguise"] = "Маскировка"
L["Jewelcrafting"] = "Ювелирное дело"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "zhCN", 4
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "炼金术"
L["Blacksmithing"] = "锻造"
L["Cooking"] = "烹饪"
L["Enchanting"] = "附魔"
L["Engineering"] = "工程学"
L["First Aid"] = "急救"
L["Leatherworking"] = "制皮"
L["Mining"] = "采矿"
L["Poisons"] = "毒药"
L["Tailoring"] = "裁缝"
if lib.env.is_turtle_wow then
L["Disguise"] = "伪装术"
L["Jewelcrafting"] = "珠宝加工"
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
local lib = LibCraftsGetLibrary()
local name, locale, version = "Locales-Professions", "zhTW", 4
local module = --[[---@type LcLocaleModule]] lib:RegisterLocaleModule(name, locale, version)
if not module then return end
local L = module:GetStrings()
L["Alchemy"] = "煉金術"
L["Blacksmithing"] = "鍛造"
L["Cooking"] = "烹飪"
L["Enchanting"] = "附魔"
L["Engineering"] = "工程學"
L["First Aid"] = "急救"
L["Leatherworking"] = "製皮"
L["Mining"] = "採礦"
L["Poisons"] = "毒藥"
L["Tailoring"] = "裁縫"
if lib.env.is_turtle_wow then
L["Disguise"] = "偽裝術"
L["Jewelcrafting"] = "珠寶設計"
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,657 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-Cooking", 4
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "Cooking")
if not module then return end
local Quality = lib.constants.qualities
local SpellSource = lib.constants.spell_sources
local RecipeSource = lib.constants.recipe_sources
module:NewCraft(2538, "Charred Wolf Meat", 1, {SpellSource.LearnedAutomatically})
:SetResult(2679)
:AddReagent(2672, 1) -- Stringy Wolf Meat
:Save()
module:NewCraft(2539, "Spiced Wolf Meat", 10, {SpellSource.Trainer})
:SetResult(2680)
:AddReagent(2672, 1) -- Stringy Wolf Meat
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(2540, "Roasted Boar Meat", 1, {SpellSource.LearnedAutomatically})
:SetResult(2681)
:AddReagent(769, 1) -- Chunk of Boar Meat
:Save()
module:NewCraft(2541, "Coyote Steak", 50, {SpellSource.Trainer})
:SetResult(2684)
:AddReagent(2673, 1) -- Coyote Meat
:Save()
module:NewCraft(2542, "Goretusk Liver Pie", 50, {})
:SetResult(724)
:AddRecipe(2697, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(723, 1) -- Goretusk Liver
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(2543, "Westfall Stew", 75, {})
:SetResult(733)
:AddRecipe(728, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(729, 1) -- Stringy Vulture Meat
:AddReagent(730, 1) -- Murloc Eye
:AddReagent(731, 1) -- Goretusk Snout
:Save()
module:NewCraft(2544, "Crab Cake", 75, {SpellSource.Trainer})
:SetResult(2683)
:AddReagent(2674, 1) -- Crawler Meat
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(2545, "Cooked Crab Claw", 85, {})
:SetResult(2682)
:AddRecipe(2698, Quality.Common, {RecipeSource.Drop, RecipeSource.Vendor})
:AddReagent(2675, 1) -- Crawler Claw
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(2546, "Dry Pork Ribs", 80, {SpellSource.Trainer})
:SetResult(2687)
:AddReagent(2677, 1) -- Boar Ribs
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(2547, "Redridge Goulash", 100, {})
:SetResult(1082)
:AddRecipe(2699, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(1080, 1) -- Tough Condor Meat
:AddReagent(1081, 1) -- Crisp Spider Meat
:Save()
module:NewCraft(2548, "Succulent Pork Ribs", 110, {})
:SetResult(2685)
:AddRecipe(2700, Quality.Common, {RecipeSource.Drop, RecipeSource.Vendor})
:AddReagent(2677, 2) -- Boar Ribs
:AddReagent(2692, 1) -- Hot Spices
:Save()
module:NewCraft(2549, "Seasoned Wolf Kabob", 100, {})
:SetResult(1017)
:AddRecipe(2701, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(1015, 2) -- Lean Wolf Flank
:AddReagent(2665, 1) -- Stormwind Seasoning Herbs
:Save()
module:NewCraft(2795, "Beer Basted Boar Ribs", 25, {})
:SetResult(2888)
:AddRecipe(2889, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2886, 1) -- Crag Boar Rib
:AddReagent(2894, 1) -- Rhapsody Malt
:Save()
module:NewCraft(3370, "Crocolisk Steak", 80, {})
:SetResult(3662)
:AddRecipe(3678, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(2924, 1) -- Crocolisk Meat
:Save()
module:NewCraft(3371, "Blood Sausage", 60, {})
:SetResult(3220)
:AddRecipe(3679, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(3172, 1) -- Boar Intestines
:AddReagent(3173, 1) -- Bear Meat
:AddReagent(3174, 1) -- Spider Ichor
:Save()
module:NewCraft(3372, "Murloc Fin Soup", 90, {})
:SetResult(3663)
:AddRecipe(3680, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(1468, 2) -- Murloc Fin
:AddReagent(2692, 1) -- Hot Spices
:Save()
module:NewCraft(3373, "Crocolisk Gumbo", 120, {})
:SetResult(3664)
:AddRecipe(3681, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3667, 1) -- Tender Crocolisk Meat
:Save()
module:NewCraft(3376, "Curiously Tasty Omelet", 130, {})
:SetResult(3665)
:AddRecipe(3682, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3685, 1) -- Raptor Egg
:Save()
module:NewCraft(3377, "Gooey Spider Cake", 110, {})
:SetResult(3666)
:AddRecipe(3683, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2251, 2) -- Gooey Spider Leg
:AddReagent(2692, 1) -- Hot Spices
:Save()
module:NewCraft(3397, "Big Bear Steak", 110, {})
:SetResult(3726)
:AddRecipe(3734, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3730, 1) -- Big Bear Meat
:Save()
module:NewCraft(3398, "Hot Lion Chops", 125, {})
:SetResult(3727)
:AddRecipe(3735, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3731, 1) -- Lion Meat
:Save()
module:NewCraft(3399, "Tasty Lion Steak", 150, {})
:SetResult(3728)
:AddRecipe(3736, Quality.Common, {RecipeSource.Quest})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(3731, 2) -- Lion Meat
:Save()
module:NewCraft(3400, "Soothing Turtle Bisque", 175, {})
:SetResult(3729)
:AddRecipe(3737, Quality.Common, {RecipeSource.Quest})
:AddReagent(3712, 1) -- Turtle Meat
:AddReagent(3713, 1) -- Soothing Spices
:Save()
module:NewCraft(4094, "Barbecued Buzzard Wing", 175, {SpellSource.Trainer})
:SetResult(4457)
:AddRecipe(4609, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3404, 1) -- Buzzard Wing
:Save()
module:NewCraft(6412, "Kaldorei Spider Kabob", 10, {})
:SetResult(5472)
:AddRecipe(5482, Quality.Common, {RecipeSource.Quest})
:AddReagent(5465, 1) -- Small Spider Leg
:Save()
module:NewCraft(6413, "Scorpid Surprise", 20, {})
:SetResult(5473)
:AddRecipe(5483, Quality.Common, {RecipeSource.Vendor})
:AddReagent(5466, 1) -- Scorpid Stinger
:Save()
module:NewCraft(6414, "Roasted Kodo Meat", 35, {})
:SetResult(5474)
:AddRecipe(5484, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(5467, 1) -- Kodo Meat
:Save()
module:NewCraft(6415, "Fillet of Frenzy", 50, {})
:SetResult(5476)
:AddRecipe(5485, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(5468, 1) -- Soft Frenzy Flesh
:Save()
module:NewCraft(6416, "Strider Stew", 50, {})
:SetResult(5477)
:AddRecipe(5486, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(4536, 1) -- Shiny Red Apple
:AddReagent(5469, 1) -- Strider Meat
:Save()
module:NewCraft(6417, "Dig Rat Stew", 90, {})
:SetResult(5478)
:AddRecipe(5487, Quality.Common, {RecipeSource.Quest})
:AddReagent(5051, 1) -- Dig Rat
:Save()
module:NewCraft(6418, "Crispy Lizard Tail", 100, {})
:SetResult(5479)
:AddRecipe(5488, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(5470, 1) -- Thunder Lizard Tail
:Save()
module:NewCraft(6419, "Lean Venison", 110, {})
:SetResult(5480)
:AddRecipe(5489, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 4) -- Mild Spices
:AddReagent(5471, 1) -- Stag Meat
:Save()
module:NewCraft(6499, "Boiled Clams", 50, {SpellSource.Trainer})
:SetResult(5525)
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(5503, 1) -- Clam Meat
:Save()
module:NewCraft(6500, "Goblin Deviled Clams", 125, {SpellSource.Trainer})
:SetResult(5527)
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(5504, 1) -- Tangy Clam Meat
:Save()
module:NewCraft(6501, "Clam Chowder", 90, {})
:SetResult(5526)
:AddRecipe(5528, Quality.Common, {RecipeSource.Vendor})
:AddReagent(1179, 1) -- Ice Cold Milk
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(5503, 1) -- Clam Meat
:Save()
module:NewCraft(7213, "Giant Clam Scorcho", 175, {})
:SetResult(6038)
:AddRecipe(6039, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(4655, 1) -- Giant Clam Meat
:Save()
module:NewCraft(7751, "Brilliant Smallfish", 1, {})
:SetResult(6290)
:AddRecipe(6325, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6291, 1) -- Raw Brilliant Smallfish
:Save()
module:NewCraft(7752, "Slitherskin Mackerel", 1, {})
:SetResult(787)
:AddRecipe(6326, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6303, 1) -- Raw Slitherskin Mackerel
:Save()
module:NewCraft(7753, "Longjaw Mud Snapper", 50, {})
:SetResult(4592)
:AddRecipe(6328, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6289, 1) -- Raw Longjaw Mud Snapper
:Save()
module:NewCraft(7754, "Loch Frenzy Delight", 50, {})
:SetResult(6316)
:AddRecipe(6329, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(6317, 1) -- Raw Loch Frenzy
:Save()
module:NewCraft(7755, "Bristle Whisker Catfish", 100, {})
:SetResult(4593)
:AddRecipe(6330, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6308, 1) -- Raw Bristle Whisker Catfish
:Save()
module:NewCraft(7827, "Rainbow Fin Albacore", 50, {})
:SetResult(5095)
:AddRecipe(6368, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6361, 1) -- Raw Rainbow Fin Albacore
:Save()
module:NewCraft(7828, "Rockscale Cod", 175, {})
:SetResult(4594)
:AddRecipe(6369, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6362, 1) -- Raw Rockscale Cod
:Save()
module:NewCraft(8238, "Savory Deviate Delight", 85, {})
:SetResult(6657)
:AddRecipe(6661, Quality.Uncommon, {RecipeSource.Drop})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(6522, 1) -- Deviate Fish
:Save()
module:NewCraft(8604, "Herb Baked Egg", 1, {SpellSource.LearnedAutomatically})
:SetResult(6888)
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(6889, 1) -- Small Egg
:Save()
module:NewCraft(8607, "Smoked Bear Meat", 40, {})
:SetResult(6890)
:AddRecipe(6892, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3173, 1) -- Bear Meat
:Save()
module:NewCraft(9513, "Thistle Tea", 60, {})
:SetResult(7676)
:AddRecipe(7678, Quality.Common, {RecipeSource.Quest})
:AddRecipe(18160, Quality.Uncommon, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(2452, 1) -- Swiftthistle
:Save()
module:NewCraft(13028, "Goldthorn Tea", 175, {SpellSource.Trainer})
:SetResult(10841)
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(3821, 1) -- Goldthorn
:Save()
module:NewCraft(15853, "Lean Wolf Steak", 125, {})
:SetResult(12209)
:AddRecipe(12227, Quality.Common, {RecipeSource.Vendor})
:AddReagent(1015, 1) -- Lean Wolf Flank
:AddReagent(2678, 1) -- Mild Spices
:Save()
module:NewCraft(15855, "Roast Raptor", 175, {})
:SetResult(12210)
:AddRecipe(12228, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(12184, 1) -- Raptor Flesh
:Save()
module:NewCraft(15856, "Hot Wolf Ribs", 175, {})
:SetResult(13851)
:AddRecipe(12229, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(12203, 1) -- Red Wolf Meat
:Save()
module:NewCraft(15861, "Jungle Stew", 175, {})
:SetResult(12212)
:AddRecipe(12231, Quality.Common, {RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(4536, 2) -- Shiny Red Apple
:AddReagent(12202, 1) -- Tiger Meat
:Save()
module:NewCraft(15863, "Carrion Surprise", 175, {})
:SetResult(12213)
:AddRecipe(12232, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(12037, 1) -- Mystery Meat
:Save()
module:NewCraft(15865, "Mystery Stew", 175, {})
:SetResult(12214)
:AddRecipe(12233, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2596, 1) -- Skin of Dwarven Stout
:AddReagent(12037, 1) -- Mystery Meat
:Save()
module:NewCraft(15906, "Dragonbreath Chili", 200, {})
:SetResult(12217)
:AddRecipe(12239, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(4402, 1) -- Small Flame Sac
:AddReagent(12037, 1) -- Mystery Meat
:Save()
module:NewCraft(15910, "Heavy Kodo Stew", 200, {})
:SetResult(12215)
:AddRecipe(12240, Quality.Common, {RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(12204, 2) -- Heavy Kodo Meat
:Save()
module:NewCraft(15915, "Spiced Chili Crab", 225, {})
:SetResult(12216)
:AddRecipe(16111, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 2) -- Hot Spices
:AddReagent(12206, 1) -- Tender Crab Meat
:Save()
module:NewCraft(15933, "Monster Omelet", 225, {})
:SetResult(12218)
:AddRecipe(16110, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3713, 2) -- Soothing Spices
:AddReagent(12207, 1) -- Giant Egg
:Save()
module:NewCraft(15935, "Crispy Bat Wing", 1, {})
:SetResult(12224)
:AddRecipe(12226, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(12223, 1) -- Meaty Bat Wing
:Save()
module:NewCraft(18238, "Spotted Yellowtail", 225, {})
:SetResult(6887)
:AddRecipe(13939, Quality.Common, {RecipeSource.Vendor})
:AddReagent(4603, 1) -- Raw Spotted Yellowtail
:Save()
module:NewCraft(18239, "Cooked Glossy Mightfish", 225, {})
:SetResult(13927)
:AddRecipe(13940, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13754, 1) -- Raw Glossy Mightfish
:Save()
module:NewCraft(18240, "Grilled Squid", 240, {})
:SetResult(13928)
:AddRecipe(13942, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13755, 1) -- Winter Squid
:Save()
module:NewCraft(18241, "Filet of Redgill", 225, {})
:SetResult(13930)
:AddRecipe(13941, Quality.Common, {RecipeSource.Vendor})
:AddReagent(13758, 1) -- Raw Redgill
:Save()
module:NewCraft(18242, "Hot Smoked Bass", 240, {})
:SetResult(13929)
:AddRecipe(13943, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 2) -- Hot Spices
:AddReagent(13756, 1) -- Raw Summer Bass
:Save()
module:NewCraft(18243, "Nightfin Soup", 250, {})
:SetResult(13931)
:AddRecipe(13945, Quality.Common, {RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(13759, 1) -- Raw Nightfin Snapper
:Save()
module:NewCraft(18244, "Poached Sunscale Salmon", 250, {})
:SetResult(13932)
:AddRecipe(13946, Quality.Common, {RecipeSource.Vendor})
:AddReagent(13760, 1) -- Raw Sunscale Salmon
:Save()
module:NewCraft(18245, "Lobster Stew", 275, {})
:SetResult(13933)
:AddRecipe(13947, Quality.Common, {RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(13888, 1) -- Darkclaw Lobster
:Save()
module:NewCraft(18246, "Mightfish Steak", 275, {})
:SetResult(13934)
:AddRecipe(13948, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13893, 1) -- Large Raw Mightfish
:Save()
module:NewCraft(18247, "Baked Salmon", 275, {})
:SetResult(13935)
:AddRecipe(13949, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13889, 1) -- Raw Whitescale Salmon
:Save()
module:NewCraft(20626, "Undermine Clam Chowder", 225, {})
:SetResult(16766)
:AddRecipe(16767, Quality.Common, {RecipeSource.Vendor})
:AddReagent(1179, 1) -- Ice Cold Milk
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(7974, 2) -- Zesty Clam Meat
:Save()
module:NewCraft(20916, "Mithril Headed Trout", 175, {})
:SetResult(8364)
:AddRecipe(17062, Quality.Common, {RecipeSource.Vendor})
:AddReagent(8365, 1) -- Raw Mithril Head Trout
:Save()
module:NewCraft(21143, "Gingerbread Cookie", 1, {})
:SetResult(17197)
:AddRecipe(17200, Quality.Common, {RecipeSource.Vendor})
:AddReagent(6889, 1) -- Small Egg
:AddReagent(17194, 1) -- Holiday Spices
:Save()
module:NewCraft(21144, "Egg Nog", 35, {})
:SetResult(17198)
:AddRecipe(17201, Quality.Common, {RecipeSource.Vendor})
:AddReagent(1179, 1) -- Ice Cold Milk
:AddReagent(6889, 1) -- Small Egg
:AddReagent(17194, 1) -- Holiday Spices
:AddReagent(17196, 1) -- Holiday Spirits
:Save()
module:NewCraft(21175, "Spider Sausage", 200, {SpellSource.Trainer})
:SetResult(17222)
:AddReagent(12205, 2) -- White Spider Meat
:Save()
module:NewCraft(22480, "Tender Wolf Steak", 225, {})
:SetResult(18045)
:AddRecipe(18046, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(12208, 1) -- Tender Wolf Meat
:Save()
module:NewCraft(22761, "Runn Tum Tuber Surprise", 275, {})
:SetResult(18254)
:AddRecipe(18267, Quality.Uncommon, {RecipeSource.Drop})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(18255, 1) -- Runn Tum Tuber
:Save()
module:NewCraft(24418, "Heavy Crocolisk Stew", 150, {})
:SetResult(20074)
:AddRecipe(20075, Quality.Common, {RecipeSource.Vendor})
:AddReagent(3667, 2) -- Tender Crocolisk Meat
:AddReagent(3713, 1) -- Soothing Spices
:Save()
module:NewCraft(24801, "Smoked Desert Dumplings", 285, {SpellSource.Quest})
:SetResult(20452)
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(20424, 1) -- Sandworm Meat
:Save()
module:NewCraft(25659, "Dirge's Kickin' Chimaerok Chops", 300, {})
:SetResult(21023)
:AddRecipe(21025, Quality.Epic, {RecipeSource.Quest})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(8150, 1) -- Deeprock Salt
:AddReagent(9061, 1) -- Goblin Rocket Fuel
:AddReagent(21024, 1) -- Chimaerok Tenderloin
:Save()
module:NewCraft(25704, "Smoked Sagefish", 80, {})
:SetResult(21072)
:AddRecipe(21099, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2678, 1) -- Mild Spices
:AddReagent(21071, 1) -- Raw Sagefish
:Save()
module:NewCraft(25954, "Sagefish Delight", 175, {})
:SetResult(21217)
:AddRecipe(21219, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(21153, 1) -- Raw Greater Sagefish
:Save()
if lib.env.is_turtle_wow then
module:NewCraft(4094, "Barbecued Buzzard Wing", 175, {})
:SetResult(4457)
:AddRecipe(4609, Quality.Common, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3404, 1) -- Buzzard Wing
:Save()
module:NewCraft(9513, "Thistle Tea", 60, {})
:SetResult(7676)
:AddRecipe(18160, Quality.Uncommon, {RecipeSource.Quest, RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(2452, 1) -- Swiftthistle
:Save()
module:NewCraft(45054, "Maritime Gumbo", 35, {})
:SetResult(30818)
:AddRecipe(30819, Quality.Common, {RecipeSource.Quest})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(2674, 1) -- Crawler Meat
:Save()
module:NewCraft(45625, "Le Fishe Au Chocolat", 300, {})
:SetResult(84040)
:AddRecipe(61666, Quality.Epic, {RecipeSource.Quest})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13464, 1) -- Golden Sansam
:AddReagent(13889, 1) -- Raw Whitescale Salmon
:AddReagent(61173, 1) -- Premium Chocolate
:Save()
module:NewCraft(45627, "Gilneas Hot Stew", 200, {})
:SetResult(84041)
:AddRecipe(61676, Quality.Uncommon, {RecipeSource.Vendor})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(12203, 1) -- Red Wolf Meat
:AddReagent(12205, 1) -- White Spider Meat
:Save()
module:NewCraft(46085, "Gurubashi Gumbo", 300, {})
:SetResult(53015)
:AddRecipe(53016, Quality.Rare, {RecipeSource.Quest})
:AddReagent(159, 1) -- Refreshing Spring Water
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(3667, 1) -- Tender Crocolisk Meat
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(12037, 2) -- Mystery Meat
:AddReagent(12202, 1) -- Tiger Meat
:Save()
module:NewCraft(49551, "Empowering Herbal Salad", 300, {})
:SetResult(83309)
:AddRecipe(92045, Quality.Rare, {RecipeSource.Quest})
:AddReagent(8838, 1) -- Sungrass
:AddReagent(22529, 1) -- Savage Frond
:AddReagent(51714, 2) -- Sweet Mountain Berry
:Save()
module:NewCraft(57047, "Danonzo's Tel'Abim Surprise", 300, {})
:SetResult(60976)
:AddRecipe(60979, Quality.Rare, {RecipeSource.Quest})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(10286, 1) -- Heart of the Wild
:AddReagent(60955, 1) -- Gargantuan Tel'Abim Banana
:Save()
module:NewCraft(57049, "Danonzo's Tel'Abim Delight", 300, {})
:SetResult(60977)
:AddRecipe(60980, Quality.Rare, {RecipeSource.Quest})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13467, 1) -- Icecap
:AddReagent(60955, 1) -- Gargantuan Tel'Abim Banana
:Save()
module:NewCraft(57051, "Danonzo's Tel'Abim Medley", 300, {})
:SetResult(60978)
:AddRecipe(60981, Quality.Rare, {RecipeSource.Quest})
:AddReagent(3713, 1) -- Soothing Spices
:AddReagent(13464, 2) -- Golden Sansam
:AddReagent(60955, 1) -- Gargantuan Tel'Abim Banana
:Save()
module:NewCraft(58044, "Ambersap Glazed Boar Ribs", 175, {})
:SetResult(41674)
:AddRecipe(19670, Quality.Uncommon, {RecipeSource.Vendor})
:AddReagent(2677, 1) -- Boar Ribs
:AddReagent(2692, 1) -- Hot Spices
:AddReagent(41675, 1) -- Ambersap
:Save()
module:NewCraft(58046, "Crawford Apple Tarte", 175, {})
:SetResult(41673)
:AddRecipe(19671, Quality.Uncommon, {}) -- TODO add source
:AddReagent(1179, 1) -- Ice Cold Milk
:AddReagent(4539, 1) -- Goldenbark Apple
:AddReagent(41677, 1) -- Northwind Flour
:Save()
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-FirstAid", 4
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "First Aid")
if not module then return end
local Quality = lib.constants.qualities
local SpellSource = lib.constants.spell_sources
local RecipeSource = lib.constants.recipe_sources
module:NewCraft(3275, "Linen Bandage", 1, {SpellSource.LearnedAutomatically})
:SetResult(1251)
:AddReagent(2589, 1) -- Linen Cloth
:Save()
module:NewCraft(3276, "Heavy Linen Bandage", 40, {SpellSource.Trainer})
:SetResult(2581)
:AddReagent(2589, 2) -- Linen Cloth
:Save()
module:NewCraft(3277, "Wool Bandage", 80, {SpellSource.Trainer})
:SetResult(3530)
:AddReagent(2592, 1) -- Wool Cloth
:Save()
module:NewCraft(3278, "Heavy Wool Bandage", 115, {SpellSource.Trainer})
:SetResult(3531)
:AddReagent(2592, 2) -- Wool Cloth
:Save()
module:NewCraft(7928, "Silk Bandage", 150, {SpellSource.Trainer})
:SetResult(6450)
:AddReagent(4306, 1) -- Silk Cloth
:Save()
module:NewCraft(7929, "Heavy Silk Bandage", 180, {})
:SetResult(6451)
:AddRecipe(16112, Quality.Common, {RecipeSource.Vendor})
:AddReagent(4306, 2) -- Silk Cloth
:Save()
module:NewCraft(7934, "Anti-Venom", 80, {SpellSource.Trainer})
:SetResult(6452)
:AddReagent(1475, 1) -- Small Venom Sac
:Save()
module:NewCraft(7935, "Strong Anti-Venom", 130, {})
:SetResult(6453)
:AddRecipe(6454, Quality.Uncommon, {RecipeSource.Chest, RecipeSource.Drop})
:AddReagent(1288, 1) -- Large Venom Sac
:Save()
module:NewCraft(10840, "Mageweave Bandage", 210, {})
:SetResult(8544)
:AddRecipe(16113, Quality.Common, {RecipeSource.Vendor})
:AddReagent(4338, 1) -- Mageweave Cloth
:Save()
module:NewCraft(10841, "Heavy Mageweave Bandage", 240, {SpellSource.Trainer})
:SetResult(8545)
:AddReagent(4338, 2) -- Mageweave Cloth
:Save()
module:NewCraft(18629, "Runecloth Bandage", 260, {SpellSource.Trainer})
:SetResult(14529)
:AddReagent(14047, 1) -- Runecloth
:Save()
module:NewCraft(18630, "Heavy Runecloth Bandage", 290, {SpellSource.Trainer})
:SetResult(14530)
:AddReagent(14047, 2) -- Runecloth
:Save()
module:NewCraft(23787, "Powerful Anti-Venom", 300, {})
:SetResult(19440)
:AddRecipe(19442, Quality.Common, {RecipeSource.Vendor})
:AddReagent(19441, 1) -- Huge Venom Sac
:Save()
if lib.env.is_turtle_wow then
module:NewCraft(10844, "Powerful Smelling Salts", 250, {})
:SetResult(8546)
:AddRecipe(8547, Quality.Rare, {RecipeSource.Drop})
:AddReagent(7078, 2) -- Essence of Fire
:AddReagent(8150, 4) -- Deeprock Salt
:AddReagent(18512, 1) -- Larval Acid
:Save()
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,83 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-Mining", 2
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "Mining")
if not module then return end
local Quality = lib.constants.qualities
local SpellSource = lib.constants.spell_sources
local RecipeSource = lib.constants.recipe_sources
module:NewCraft(2657, "Smelt Copper", 1, {SpellSource.LearnedAutomatically})
:SetResult(2840)
:AddReagent(2770, 1) -- Copper Ore
:Save()
module:NewCraft(2658, "Smelt Silver", 75, {SpellSource.Trainer})
:SetResult(2842)
:AddReagent(2775, 1) -- Silver Ore
:Save()
module:NewCraft(2659, "Smelt Bronze", 65, {SpellSource.Trainer})
:SetResult(2841)
:AddReagent(2840, 1) -- Copper Bar
:AddReagent(3576, 1) -- Tin Bar
:Save()
module:NewCraft(3304, "Smelt Tin", 65, {SpellSource.Trainer})
:SetResult(3576)
:AddReagent(2771, 1) -- Tin Ore
:Save()
module:NewCraft(3307, "Smelt Iron", 125, {SpellSource.Trainer})
:SetResult(3575)
:AddReagent(2772, 1) -- Iron Ore
:Save()
module:NewCraft(3308, "Smelt Gold", 155, {SpellSource.Trainer})
:SetResult(3577)
:AddReagent(2776, 1) -- Gold Ore
:Save()
module:NewCraft(3569, "Smelt Steel", 165, {SpellSource.Trainer})
:SetResult(3859)
:AddReagent(3575, 1) -- Iron Bar
:AddReagent(3857, 1) -- Coal
:Save()
module:NewCraft(10097, "Smelt Mithril", 175, {SpellSource.Trainer})
:SetResult(3860)
:AddReagent(3858, 1) -- Mithril Ore
:Save()
module:NewCraft(10098, "Smelt Truesilver", 230, {SpellSource.Trainer})
:SetResult(6037)
:AddReagent(7911, 1) -- Truesilver Ore
:Save()
module:NewCraft(14891, "Smelt Dark Iron", 230, {SpellSource.Quest})
:SetResult(11371)
:AddReagent(11370, 8) -- Dark Iron Ore
:Save()
module:NewCraft(16153, "Smelt Thorium", 250, {SpellSource.Trainer})
:SetResult(12359)
:AddReagent(10620, 1) -- Thorium Ore
:Save()
module:NewCraft(22967, "Smelt Elementium", 300, {SpellSource.Trainer})
:SetResult(17771)
:AddReagent(12360, 10) -- Arcanite Bar
:AddReagent(17010, 1) -- Fiery Core
:AddReagent(18562, 1) -- Elementium Ore
:AddReagent(18567, 3) -- Elemental Flux
:Save()
if lib.env.is_turtle_wow then
module:NewCraft(45451, "Smelt Dreamsteel", 300, {})
:SetResult(61216)
:AddRecipe(61226, Quality.Uncommon, {RecipeSource.Quest})
:AddReagent(3859, 1) -- Steel Bar
:AddReagent(20381, 1) -- Dreamscale
:AddReagent(61198, 1) -- Small Dream Shard
:Save()
end
@@ -0,0 +1,178 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-Poisons", 4
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "Poisons")
if not module then return end
local Quality = lib.constants.qualities
local SpellSource = lib.constants.spell_sources
local RecipeSource = lib.constants.recipe_sources
module:NewCraft(2835, "Deadly Poison", 130, {SpellSource.Trainer})
:SetResult(2892)
:AddReagent(3372, 1) -- Leaded Vial
:AddReagent(5173, 1) -- Deathweed
:Save()
module:NewCraft(2837, "Deadly Poison II", 170, {SpellSource.Trainer})
:SetResult(2893)
:AddReagent(3372, 1) -- Leaded Vial
:AddReagent(5173, 2) -- Deathweed
:Save()
module:NewCraft(3420, "Crippling Poison", 1, {SpellSource.Trainer})
:SetResult(3775)
:AddReagent(2930, 1) -- Essence of Pain
:AddReagent(3371, 1) -- Empty Vial
:Save()
module:NewCraft(3421, "Crippling Poison II", 230, {SpellSource.Trainer})
:SetResult(3776)
:AddReagent(8923, 3) -- Essence of Agony
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(5763, "Mind-numbing Poison", 100, {SpellSource.Trainer})
:SetResult(5237)
:AddReagent(2928, 1) -- Dust of Decay
:AddReagent(2930, 1) -- Essence of Pain
:AddReagent(3371, 1) -- Empty Vial
:Save()
module:NewCraft(6510, "Blinding Powder", 150, {SpellSource.Trainer})
:SetResult(5530)
:AddReagent(3818, 1) -- Fadeleaf
:Save()
module:NewCraft(8681, "Instant Poison", 1, {SpellSource.LearnedAutomatically})
:SetResult(6947)
:AddReagent(2928, 1) -- Dust of Decay
:AddReagent(3371, 1) -- Empty Vial
:Save()
module:NewCraft(8687, "Instant Poison II", 120, {SpellSource.Trainer})
:SetResult(6949)
:AddReagent(2928, 3) -- Dust of Decay
:AddReagent(3372, 1) -- Leaded Vial
:Save()
module:NewCraft(8691, "Instant Poison III", 160, {SpellSource.Trainer})
:SetResult(6950)
:AddReagent(3372, 1) -- Leaded Vial
:AddReagent(8924, 1) -- Dust of Deterioration
:Save()
module:NewCraft(8694, "Mind-numbing Poison II", 170, {SpellSource.Trainer})
:SetResult(6951)
:AddReagent(2928, 4) -- Dust of Decay
:AddReagent(2930, 4) -- Essence of Pain
:AddReagent(3372, 1) -- Leaded Vial
:Save()
module:NewCraft(11341, "Instant Poison IV", 200, {SpellSource.Trainer})
:SetResult(8926)
:AddReagent(8924, 2) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(11342, "Instant Poison V", 240, {SpellSource.Trainer})
:SetResult(8927)
:AddReagent(8924, 3) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(11343, "Instant Poison VI", 280, {SpellSource.Trainer})
:SetResult(8928)
:AddReagent(8924, 4) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(11357, "Deadly Poison III", 210, {SpellSource.Trainer})
:SetResult(8984)
:AddReagent(5173, 3) -- Deathweed
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(11358, "Deadly Poison IV", 250, {SpellSource.Trainer})
:SetResult(8985)
:AddReagent(5173, 5) -- Deathweed
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(11400, "Mind-numbing Poison III", 240, {SpellSource.Trainer})
:SetResult(9186)
:AddReagent(8923, 2) -- Essence of Agony
:AddReagent(8924, 2) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(13220, "Wound Poison", 140, {SpellSource.Trainer})
:SetResult(10918)
:AddReagent(2930, 1) -- Essence of Pain
:AddReagent(3372, 1) -- Leaded Vial
:AddReagent(5173, 1) -- Deathweed
:Save()
module:NewCraft(13228, "Wound Poison II", 180, {SpellSource.Trainer})
:SetResult(10920)
:AddReagent(2930, 1) -- Essence of Pain
:AddReagent(3372, 1) -- Leaded Vial
:AddReagent(5173, 2) -- Deathweed
:Save()
module:NewCraft(13229, "Wound Poison III", 220, {SpellSource.Trainer})
:SetResult(10921)
:AddReagent(5173, 2) -- Deathweed
:AddReagent(8923, 1) -- Essence of Agony
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(13230, "Wound Poison IV", 260, {SpellSource.Trainer})
:SetResult(10922)
:AddReagent(5173, 2) -- Deathweed
:AddReagent(8923, 2) -- Essence of Agony
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(25347, "Deadly Poison V", 300, {})
:SetResult(20844)
:AddRecipe(21302, Quality.Rare, {RecipeSource.Drop})
:AddReagent(5173, 7) -- Deathweed
:AddReagent(8925, 1) -- Crystal Vial
:Save()
if lib.env.is_turtle_wow then
module:NewCraft(45611, "Agitating Poison", 300, {SpellSource.Trainer})
:SetResult(65032)
:AddReagent(2931, 2) -- Maiden's Anguish
:AddReagent(3372, 1) -- Leaded Vial
:Save()
module:NewCraft(45878, "Dissolvent Poison", 240, {SpellSource.Trainer})
:SetResult(54009)
:AddReagent(2931, 3) -- Maiden's Anguish
:AddReagent(8924, 2) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(45882, "Dissolvent Poison II", 300, {SpellSource.Trainer})
:SetResult(54010)
:AddReagent(2931, 4) -- Maiden's Anguish
:AddReagent(8924, 3) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(51924, "Corrosive Poison", 260, {SpellSource.Trainer})
:SetResult(47408)
:AddReagent(5173, 3) -- Deathweed
:AddReagent(8924, 3) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
module:NewCraft(52576, "Corrosive Poison II", 300, {})
:SetResult(47409)
:AddRecipe(21302, Quality.Rare, {RecipeSource.Drop})
:AddReagent(5173, 3) -- Deathweed
:AddReagent(8924, 3) -- Dust of Deterioration
:AddReagent(8925, 1) -- Crystal Vial
:Save()
end
@@ -0,0 +1,13 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="Alchemy.lua" />
<Script file="Blacksmithing.lua" />
<Script file="Cooking.lua" />
<Script file="Enchanting.lua" />
<Script file="Engineering.lua" />
<Script file="FirstAid.lua" />
<Script file="Leatherworking.lua" />
<Script file="Mining.lua" />
<Script file="Poisons.lua" />
<Script file="Tailoring.lua" />
<Include file="Turtle\Professions.xml"/>
</Ui>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-Disguise", 1
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "Disguise")
if not module then return end
local Quality = lib.constants.qualities
local SpellSource = lib.constants.spell_sources
local RecipeSource = lib.constants.recipe_sources
if lib.env.is_turtle_wow then
module:NewCraft(5169, "Defias Disguise", 1, {})
:SetMinCharacterLevel(13)
:AddRecipe(5126, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2589, 1) -- Linen Cloth
:AddReagent(7997, 1) -- Red Defias Mask
:Save()
module:NewCraft(5264, "South Seas Pirate Disguise", 1, {})
:SetMinCharacterLevel(13)
:AddRecipe(5127, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2589, 1) -- Linen Cloth
:AddReagent(5107, 1) -- Deckhand's Shirt
:Save()
module:NewCraft(5265, "Stonesplinter Trogg Disguise", 1, {})
:SetMinCharacterLevel(13)
:AddRecipe(5131, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2589, 1) -- Linen Cloth
:AddReagent(5109, 1) -- Stonesplinter Rags
:Save()
module:NewCraft(5266, "Syndicate Disguise", 1, {})
:SetMinCharacterLevel(22)
:AddRecipe(5132, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2592, 1) -- Wool Cloth
:AddReagent(5113, 1) -- Mark of the Syndicate
:Save()
module:NewCraft(5267, "Dalaran Wizard Disguise", 1, {})
:SetMinCharacterLevel(13)
:AddRecipe(5130, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2589, 1) -- Linen Cloth
:AddReagent(5110, 1) -- Dalaran Wizard's Robe
:Save()
module:NewCraft(5268, "Dark Iron Dwarf Disguise", 1, {})
:SetMinCharacterLevel(22)
:AddRecipe(5129, Quality.Common, {RecipeSource.Vendor})
:AddReagent(2592, 1) -- Wool Cloth
:AddReagent(5108, 1) -- Dark Iron Leather
:Save()
module:NewCraft(5668, "Peasant Disguise", 1, {SpellSource.LearnedAutomatically})
:SetMinCharacterLevel(2)
:AddReagent(2589, 1) -- Linen Cloth
:Save()
module:NewCraft(5669, "Peon Disguise", 1, {SpellSource.LearnedAutomatically})
:SetMinCharacterLevel(2)
:AddReagent(2589, 1) -- Linen Cloth
:Save()
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="Disguise.lua" />
<Script file="Jewelcrafting.lua" />
<Script file="Survival.lua" />
</Ui>
@@ -0,0 +1,265 @@
local lib = LibCraftsGetLibrary()
local name, version = "Professions-Survival", 6
local module = --[[---@type LcProfessionModule]] lib:RegisterProfessionModule(name, version, "Survival")
if not module then return end
local SpellSource = lib.constants.spell_sources
--[[
Survival is a Turtle WoW secondary profession, trained at Nesingwary's
Expedition (Stranglethorn Vale). This is now the full recipe list, skill 1
through 300, triple-confirmed: a player-provided skill-up table, that same
player's own "/gmc dump" of their known trade skill entries (real result
item ids), and a follow-up "/gmc dump" of both trainer NPCs' full service
lists (every recipe on offer, known or not, with skill requirements - just
no item ids, since training a spell has no crafted-item preview). Two
skill-295 entries the original table listed with source "Recipe" rather than
"Trainer" (Oil-Powered Cooker, Prospector's Magnifying Lens) are the only
known gap - they're taught by a recipe item this addon has no id for, not by
the trainer directly, so they're left out rather than mis-tagged.
The client API has no way to read a recipe's actual spell id under any of
the three windows (trade skill, craft, or trainer) - only its name and,
for known trade-skill/craft entries, the crafted item's id. The skill
155-300 result items were instead confirmed by hand against octowow.st
(https://octowow.st/db/?item=<id>) - note that db shows both an Item ID and
an unrelated Display ID (a 3D model reference) on the same page; it's the
Item ID this needs.
Because CraftRepository matches "known vs missing" by recipe NAME (see
CharacterRepository/CraftRepository), not by spell id, every recipe below
still uses a safe placeholder spell id (900001+, an id range nowhere near
any real Blizzard or Turtle spell) where the real one isn't confirmed -
7 recipes have a real one, the rest don't. That doesn't affect accuracy:
every recipe below (all 87) now has a real result item id, and the tooltip
always prefers that over the spell id (see CraftsListItem:_DrawTooltip), so
a placeholder spell id never actually surfaces anywhere.
]]
if lib.env.is_turtle_wow then
-- Confirmed real spell ids
module:NewCraft(46064, "Dim Torch", 1, {SpellSource.Trainer})
:SetResult(6182)
:Save()
module:NewCraft(47101, "Survivalist's Skinning Knife", 10, {SpellSource.Trainer})
:SetResult(7009)
:Save()
module:NewCraft(47103, "Driftwood Fishing Pole", 10, {SpellSource.Trainer})
:SetResult(7010)
:Save()
module:NewCraft(46072, "Traveler's Tent", 80, {SpellSource.Trainer})
:SetResult(51283)
:Save()
module:NewCraft(46073, "Fishing Boat", 115, {SpellSource.Trainer})
:SetResult(51282)
:Save()
module:NewCraft(46066, "Murloc's Flippers", 125, {SpellSource.Trainer})
:SetResult(65028)
:Save()
module:NewCraft(46077, "Repaired Electro-Lantern", 230, {SpellSource.Trainer})
:SetResult(65030)
:Save()
-- Placeholder spell ids (900001+) - real names/skill levels/result items,
-- see header comment
module:NewCraft(900001, "Bundle of Simple Sticks", 5, {SpellSource.Trainer})
:SetResult(42149)
:Save()
module:NewCraft(900002, "Crude Hunting Bow", 15, {SpellSource.Trainer})
:SetResult(42092)
:Save()
module:NewCraft(900003, "Crude Hatchet", 15, {SpellSource.Trainer})
:SetResult(42091)
:Save()
module:NewCraft(900004, "Crude Machete", 15, {SpellSource.Trainer})
:SetResult(42090)
:Save()
module:NewCraft(900005, "Crude Walking Stick", 15, {SpellSource.Trainer})
:SetResult(42089)
:Save()
module:NewCraft(900006, "Copper Lantern", 20, {SpellSource.Trainer})
:SetResult(42093)
:Save()
module:NewCraft(900007, "Simple Slingshot", 30, {SpellSource.Trainer})
:SetResult(42229)
:Save()
module:NewCraft(900008, "Simple Herbalist's Backpack", 40, {SpellSource.Trainer})
:SetResult(33369)
:Save()
module:NewCraft(900009, "Makeshift Rations Bag", 40, {SpellSource.Trainer})
:SetResult(33370)
:Save()
module:NewCraft(900010, "Weak Healing Salve", 50, {SpellSource.Trainer})
:SetResult(42233)
:Save()
module:NewCraft(900011, "Makeshift Knife", 60, {SpellSource.Trainer})
:SetResult(42094)
:Save()
module:NewCraft(900012, "Gardening Gloves", 65, {SpellSource.Trainer})
:SetResult(42095)
:Save()
module:NewCraft(900013, "Crude Fishing Rod", 70, {SpellSource.Trainer})
:SetResult(42096)
:Save()
module:NewCraft(900014, "Hunting Spear", 90, {SpellSource.Trainer})
:SetResult(42097)
:Save()
module:NewCraft(900015, "Gardening Broom", 95, {SpellSource.Trainer})
:SetResult(42098)
:Save()
module:NewCraft(900016, "Healing Salve", 100, {SpellSource.Trainer})
:SetResult(42125)
:Save()
module:NewCraft(900017, "Oakwood Bow", 100, {SpellSource.Trainer})
:SetResult(42099)
:Save()
module:NewCraft(900018, "Simple Fishing Lure", 100, {SpellSource.Trainer})
:SetResult(114)
:Save()
module:NewCraft(900019, "Fishing Bag", 105, {SpellSource.Trainer})
:SetResult(33371)
:Save()
module:NewCraft(900020, "Skinner's Pack", 110, {SpellSource.Trainer})
:SetResult(33372)
:Save()
module:NewCraft(900021, "Gardening Pitchfork", 120, {SpellSource.Trainer})
:SetResult(42100)
:Save()
module:NewCraft(900022, "Blackmouth Fishing Trap", 125, {SpellSource.Trainer})
:SetResult(42329)
:Save()
module:NewCraft(900023, "Bundle of Bright Wood Sticks", 125, {SpellSource.Trainer})
:SetResult(42150)
:Save()
module:NewCraft(900024, "Murloc Scale Coat", 125, {SpellSource.Trainer})
:SetResult(42101)
:Save()
module:NewCraft(900025, "Sturdy Blade", 130, {SpellSource.Trainer})
:SetResult(42104)
:Save()
module:NewCraft(900026, "Sturdy Cane", 130, {SpellSource.Trainer})
:SetResult(42102)
:Save()
module:NewCraft(900027, "Sturdy Knife", 130, {SpellSource.Trainer})
:SetResult(42103)
:Save()
module:NewCraft(900028, "Sturdy Net", 130, {SpellSource.Trainer})
:SetResult(42154)
:Save()
module:NewCraft(900029, "Reliable Fishing Rod", 135, {SpellSource.Trainer})
:SetResult(42105)
:Save()
module:NewCraft(900030, "Hat of the Junior Chef", 140, {SpellSource.Trainer})
:SetResult(42106)
:Save()
module:NewCraft(900031, "Rugged Mining Sack", 140, {SpellSource.Trainer})
:SetResult(42294)
:Save()
module:NewCraft(900032, "Throwable Net", 140, {SpellSource.Trainer})
:SetResult(42132)
:Save()
module:NewCraft(900033, "Bright Wood Arrows", 145, {SpellSource.Trainer})
:SetResult(42198)
:Save()
module:NewCraft(900034, "Treasure Compass", 145, {SpellSource.Trainer})
:SetResult(42107)
:Save()
module:NewCraft(900035, "Potent Healing Salve", 150, {SpellSource.Trainer})
:SetResult(42126)
:Save()
module:NewCraft(900036, "Spicy Fishing Lure", 150, {SpellSource.Trainer})
:SetResult(125)
:Save()
module:NewCraft(900037, "Studded Rations Bag", 150, {SpellSource.Trainer})
:SetResult(33373)
:Save()
-- Placeholder spell ids (skill 155+), but real result items now -
-- confirmed via a player pulling Item IDs (not Display IDs - a
-- different number on the same db page) off octowow.st
module:NewCraft(900038, "Slowing Bolas", 155, {SpellSource.Trainer}):SetResult(106):Save()
module:NewCraft(900039, "Iron Lantern", 160, {SpellSource.Trainer}):SetResult(2714):Save()
module:NewCraft(900040, "Snap Trap", 160, {SpellSource.Trainer}):SetResult(42328):Save()
module:NewCraft(900041, "Edged Machete", 165, {SpellSource.Trainer}):SetResult(42108):Save()
module:NewCraft(900042, "Iron Spear", 165, {SpellSource.Trainer}):SetResult(42109):Save()
module:NewCraft(900043, "Reinforced Fishing Rod", 170, {SpellSource.Trainer}):SetResult(42110):Save()
module:NewCraft(900044, "Bundle of Shade Wood Sticks", 175, {SpellSource.Trainer}):SetResult(42151):Save()
module:NewCraft(900045, "Firefin Fishing Trap", 175, {SpellSource.Trainer}):SetResult(42330):Save()
module:NewCraft(900046, "Water Trudgers", 175, {SpellSource.Trainer}):SetResult(42111):Save()
module:NewCraft(900047, "Cleaning Cloth", 180, {SpellSource.Trainer}):SetResult(60001):Save()
module:NewCraft(900048, "Sharpened Herb Sickle", 180, {SpellSource.Trainer}):SetResult(42112):Save()
module:NewCraft(900049, "Lined Wintercloak", 185, {SpellSource.Trainer}):SetResult(42113):Save()
module:NewCraft(900050, "Sleek Pinewood Bow", 190, {SpellSource.Trainer}):SetResult(42114):Save()
module:NewCraft(900051, "Savory Fishing Lure", 200, {SpellSource.Trainer}):SetResult(133):Save()
module:NewCraft(900052, "Superior Healing Salve", 200, {SpellSource.Trainer}):SetResult(42127):Save()
module:NewCraft(900053, "Nutritious Rations", 205, {SpellSource.Trainer}):SetResult(42155):Save()
module:NewCraft(900054, "Shade Wood Arrows", 205, {SpellSource.Trainer}):SetResult(42199):Save()
module:NewCraft(900055, "Hiking Staff", 215, {SpellSource.Trainer}):SetResult(42116):Save()
module:NewCraft(900056, "Tree Hatchet", 215, {SpellSource.Trainer}):SetResult(42117):Save()
module:NewCraft(900057, "Vine Cutter", 215, {SpellSource.Trainer}):SetResult(42115):Save()
module:NewCraft(900058, "Bundle of Tropical Sticks", 225, {SpellSource.Trainer}):SetResult(42152):Save()
module:NewCraft(900059, "Sunshade Hat", 235, {SpellSource.Trainer}):SetResult(42118):Save()
module:NewCraft(900060, "Thick Rations Bag", 235, {SpellSource.Trainer}):SetResult(33374):Save()
module:NewCraft(900061, "Aromatic Berries", 240, {SpellSource.Trainer}):SetResult(42131):Save()
module:NewCraft(900062, "Spiced Berries", 240, {SpellSource.Trainer}):SetResult(42130):Save()
module:NewCraft(900063, "Warped Recurve Bow", 240, {SpellSource.Trainer}):SetResult(42119):Save()
module:NewCraft(900064, "Advanced Camouflage", 245, {SpellSource.Trainer}):SetResult(42230):Save()
module:NewCraft(900065, "Emergency Parachute", 250, {SpellSource.Trainer}):SetResult(42156):Save()
module:NewCraft(900066, "Premium Fishing Lure", 250, {SpellSource.Trainer}):SetResult(145):Save()
module:NewCraft(900067, "Stabilizing Healing Salve", 250, {SpellSource.Trainer}):SetResult(42128):Save()
module:NewCraft(900068, "Smooth Ironfeather Arrows", 255, {SpellSource.Trainer}):SetResult(42200):Save()
module:NewCraft(900069, "Heavy Duty Machete", 260, {SpellSource.Trainer}):SetResult(42120):Save()
module:NewCraft(900070, "Thorium Edged Machete", 260, {SpellSource.Trainer}):SetResult(42121):Save()
module:NewCraft(900071, "Thorium Spear", 265, {SpellSource.Trainer}):SetResult(42122):Save()
module:NewCraft(900072, "Bundle of Star Wood Sticks", 270, {SpellSource.Trainer}):SetResult(42153):Save()
module:NewCraft(900073, "Razor-sharp Skinning Knife", 270, {SpellSource.Trainer}):SetResult(42124):Save()
module:NewCraft(900074, "Mastercraft Fishing Rod", 275, {SpellSource.Trainer}):SetResult(42123):Save()
module:NewCraft(900075, "Stonescale Fishing Trap", 275, {SpellSource.Trainer}):SetResult(42331):Save()
module:NewCraft(900076, "Miner's Rucksack", 280, {SpellSource.Trainer}):SetResult(33375):Save()
module:NewCraft(900077, "Herbalist's Knapsack", 285, {SpellSource.Trainer}):SetResult(33376):Save()
module:NewCraft(900078, "Skinner's Carryall", 285, {SpellSource.Trainer}):SetResult(33378):Save()
module:NewCraft(900079, "Cooling Rations Bag", 290, {SpellSource.Trainer}):SetResult(33379):Save()
module:NewCraft(900080, "Major Healing Salve", 300, {SpellSource.Trainer}):SetResult(42129):Save()
end
@@ -0,0 +1,45 @@
# LibCrafts
A modern, lightweight embeddable database library for crafting professions in Vanilla WoW (1.12.1) and [Turtle WoW](https://turtle-wow.org). Designed as an replacement (not a drop-in) for [ReagentData](https://github.com/refaim/ReagentData), [TradeSkillsData](https://github.com/refaim/TradeSkillsData) and [TradeSkillsData-turtle](https://github.com/refaim/TradeSkillsData-turtle).
**Notice to players:** This is **NOT a standalone addon** and provides no functionality on its own. **Do not install this library directly.** This is a developer resource meant to be embedded inside other addons. If you're looking for a crafting addon, this is not it. You might be looking for [MasterTradeSkills](https://github.com/refaim/MasterTradeSkills) or [MissingCrafts](https://github.com/refaim/MissingCrafts) instead.
## Features
- Lightweight ID-based database architecture
- Complete crafting system mapping (spells, recipes, reagents, results, sources etc)
- Developer-friendly database structure
- Vanilla WoW and Turtle WoW compatibility
- Localization support
## Usage
```lua
local LibCrafts = LibStub("LibCrafts-1.0")
-- Query crafts using reagent ID
local reagentId = 2318 -- Light Leather, see https://www.wowhead.com/classic/item=2318/light-leather
local crafts = LibCrafts:GetCraftsByReagentId(reagentId) -- Light Leather
---Query crafts using recipe ID
local recipeId = 4408 -- Mechanical Squirrel, see https://www.wowhead.com/classic/item=4408/schematic-mechanical-squirrel
local crafts = LibCrafts:GetCraftsByRecipeId(recipeId) -- Mechanical Squirrel
--- Query crafts using english or localized profession name
local crafts = LibCrafts:GetCraftsByProfession("Leatherworking")
local crafts = LibCrafts:GetCraftsByProfession("制皮")
```
## Changes
See [CHANGES](CHANGES.md) for the full list of changes.
## Contributing
If you have suggestions for improvements or have found a bug, please create an issue or submit a pull request.
## License
LibCrafts is distributed under the MIT License. For details, see the [LICENSE](LICENSE) file.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Roman Kharitonov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,219 @@
--[[
Name: LibItemTooltip-1.0
Developed by: Refaim (rkharito@yandex.ru, https://github.com/refaim/)
Description: A library that helps to hook into item tooltips.
Dependencies: LibStub
Compatibility: Vanilla (1.12.1), Turtle (1.17.2+)
]]
---@type LibStubDef
local LibStub = getglobal("LibStub")
assert(LibStub ~= nil)
local untyped_lib, old_lib_version = LibStub:NewLibrary("LibItemTooltip-1.0", 2)
if not untyped_lib then
return
end
---@alias LitEventHandler fun(tooltip: GameTooltip, item_link: string, item_id: number)
---@shape LitHook
---@field lib_version number
---@field name string
---@field object table
---@field orig function
---@field hook function
---@class LibItemTooltip
---@field game_tooltip_event_frame Frame
---@field item_ref_tooltip_event_frame Frame
---@field game_tooltip_item_link string|nil
---@field item_ref_tooltip_link string|nil
---@field event_to_handlers table<string, LitEventHandler[]>
---@field object_to_name_to_hook table<string, table<string, LitHook>>
local lib = --[[---@type LibItemTooltip]] untyped_lib
if lib.event_to_handlers == nil then
lib.event_to_handlers = {}
end
if lib.object_to_name_to_hook == nil then
lib.object_to_name_to_hook = {}
end
---@type table<string, string>
local event_upgrade_map = {
OnShow = "OnShow2",
}
---@param event "OnShow"
---@param handler LitEventHandler
function lib:RegisterEvent(event, handler)
assert(type(event) == "string", "Event name must be a string")
assert(type(handler) == "function", "Handler must be a function")
local new_event = event_upgrade_map[event] or event
if self.event_to_handlers[new_event] == nil then
self.event_to_handlers[new_event] = {}
end
tinsert(self.event_to_handlers[new_event], handler)
end
---@param item_link string
---@return number
local function extract_id(item_link)
local _, _, id = string.find(item_link, "item:(%d+)")
return --[[---@not nil]] tonumber(--[[---@not nil]] id)
end
---@param tooltip GameTooltip
---@param item_link string
local function on_show(tooltip, item_link)
for _, handler in ipairs(lib.event_to_handlers["OnShow2"] or {}) do
handler(tooltip, item_link, extract_id(item_link))
end
end
---@param frame Frame
local function delete_event_frame(frame)
frame:SetScript("OnShow", nil)
frame:SetScript("OnHide", nil)
frame:SetParent(nil)
end
if old_lib_version ~= nil and old_lib_version < 2 then
local old_frame = --[[---@type Frame]] (--[[---@type table]] lib).event_frame
delete_event_frame(old_frame)
for old_event, new_event in pairs(event_upgrade_map) do
if lib.event_to_handlers[new_event] == nil then
lib.event_to_handlers[new_event] = {}
end
for _, handler in ipairs(lib.event_to_handlers[old_event] or {}) do
tinsert(lib.event_to_handlers[new_event], handler)
end
lib.event_to_handlers[old_event] = nil
end
end
if lib.game_tooltip_event_frame ~= nil then
delete_event_frame(lib.game_tooltip_event_frame)
end
lib.game_tooltip_event_frame = CreateFrame("Frame", nil, GameTooltip)
lib.game_tooltip_event_frame:SetScript("OnShow", function()
if lib.game_tooltip_item_link ~= nil then
on_show(GameTooltip, --[[---@not nil]] lib.game_tooltip_item_link)
end
end)
lib.game_tooltip_event_frame:SetScript("OnHide", function()
lib.game_tooltip_item_link = nil
end)
if lib.item_ref_tooltip_event_frame ~= nil then
delete_event_frame(lib.item_ref_tooltip_event_frame)
end
lib.item_ref_tooltip_event_frame = CreateFrame("Frame", nil, ItemRefTooltip)
lib.item_ref_tooltip_event_frame:SetScript("OnShow", function()
if lib.item_ref_tooltip_link ~= nil and not IsAltKeyDown() and not IsControlKeyDown() and not IsShiftKeyDown() then
on_show(ItemRefTooltip, --[[---@not nil]] lib.item_ref_tooltip_link)
end
end)
lib.item_ref_tooltip_event_frame:SetScript("OnHide", function()
lib.item_ref_tooltip_link = nil
end)
---@param object table
---@param name string
---@param on_hook function
local function hook(object, name, on_hook)
assert(type(object[name]) == "function")
local object_id = tostring(object)
local name_to_hook = lib.object_to_name_to_hook[object_id]
if name_to_hook == nil then
name_to_hook = {}
end
local old_hook = name_to_hook[name]
if old_hook ~= nil then
old_hook.object[name] = old_hook.orig
end
---@type function
local orig = object[name]
object[name] = function(...)
on_hook(unpack(arg))
return orig(unpack(arg))
end
name_to_hook[name] = {
lib_version = 2,
name = name,
object = object,
orig = orig,
hook = on_hook
}
lib.object_to_name_to_hook[object_id] = name_to_hook
end
hook(getfenv(), "SetItemRef", function(link, text, button)
lib.item_ref_tooltip_link = link
end)
hook(GameTooltip, "SetHyperlink", function(self, link)
if string.find(link, "item:") then
lib.game_tooltip_item_link = link
end
end)
hook(GameTooltip, "SetBagItem", function(self, container, slot)
lib.game_tooltip_item_link = GetContainerItemLink(container, slot)
end)
hook(GameTooltip, "SetCraftItem", function(self, skill, slot)
lib.game_tooltip_item_link = GetCraftReagentItemLink(skill, slot)
end)
hook(GameTooltip, "SetCraftSpell", function(self, slot)
lib.game_tooltip_item_link = GetCraftItemLink(slot)
end)
hook(GameTooltip, "SetInventoryItem", function(self, unit, slot)
lib.game_tooltip_item_link = GetInventoryItemLink(unit, slot)
end)
hook(GameTooltip, "SetLootItem", function(self, slot)
lib.game_tooltip_item_link = GetLootSlotLink(slot)
end)
hook(GameTooltip, "SetLootRollItem", function(self, id)
lib.game_tooltip_item_link = GetLootRollItemLink(id)
end)
hook(GameTooltip, "SetMerchantItem", function(self, item_index)
lib.game_tooltip_item_link = GetMerchantItemLink(item_index)
end)
hook(GameTooltip, "SetQuestItem", function(self, item_type, index)
lib.game_tooltip_item_link = GetQuestItemLink(item_type, index)
end)
hook(GameTooltip, "SetQuestLogItem", function(self, item_type, index)
lib.game_tooltip_item_link = GetQuestLogItemLink(item_type, index)
end)
hook(GameTooltip, "SetTradePlayerItem", function(self, index)
lib.game_tooltip_item_link = GetTradePlayerItemLink(index)
end)
hook(GameTooltip, "SetTradeSkillItem", function(self, skill_index, reagent_index)
if reagent_index then
lib.game_tooltip_item_link = GetTradeSkillReagentItemLink(skill_index, reagent_index)
else
lib.game_tooltip_item_link = GetTradeSkillItemLink(skill_index)
end
end)
hook(GameTooltip, "SetTradeTargetItem", function(self, index)
lib.game_tooltip_item_link = GetTradeTargetItemLink(index)
end)
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="LibStub.lua" />
<Script file="LibItemTooltip-1.0.lua" />
</Ui>
@@ -0,0 +1,33 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local _G = getfenv()
local strfind, strfmt = string.find, string.format
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or { libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
local _,_,num = strfind(minor, "(%d+)")
minor = assert(tonumber(num), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(strfmt("Cannot find a library instance of %q.", tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
+33
View File
@@ -0,0 +1,33 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local _G = getfenv()
local strfind, strfmt = string.find, string.format
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or { libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
local _,_,num = strfind(minor, "(%d+)")
minor = assert(tonumber(num), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(strfmt("Cannot find a library instance of %q.", tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
+25
View File
@@ -0,0 +1,25 @@
setfenv(1, Gromissingcrafts)
---@return boolean
local function IsSuperWoWActive()
return getglobal("SetAutoloot") ~= nil or SUPERWOW_VERSION ~= nil
end
---@return number
local function GetSuperWoWVersion()
return tonumber(SUPERWOW_VERSION or "0.0") or 0
end
---@param spellId number
---@return string
function GetSpellLink(spellId)
assert(type(spellId) == "number")
local prefix = "enchant:"
if IsSuperWoWActive() and GetSuperWoWVersion() <= 1.2 then
-- https://github.com/balakethelock/SuperWoW/wiki/Changelog#07112024--12
prefix = "spell:"
end
return prefix .. tostring(spellId)
end
+168
View File
@@ -0,0 +1,168 @@
setfenv(1, Gromissingcrafts)
--[[
Turtle WoW's own trade skill / craft / trainer windows don't expose a
recipe's spell id or required skill level through the client API - the only
reliably scriptable facts are a recipe's exact name, and (trade skill/craft
only) its resulting item id. "/gmc dump" walks whichever of those three
windows is open and prints what it can, so gaps in lib/LibCrafts-1.0
(Survival's especially) can be filled in with precise lookups instead of
fuzzy name searches. The trainer window is the most complete source - it
lists every recipe the trainer offers whether you've learned it or not -
but only the trade skill/craft windows expose item ids, and only for
recipes you already know.
]]
---@class DevTools
DevTools = {}
---@param itemLink string|nil
---@return number|nil
local function extractItemId(itemLink)
if itemLink == nil then
return nil
end
local _, _, id = strfind(itemLink, "item:(%d+)")
return id and tonumber(id) or nil
end
---@param text string
local function printLine(text)
DEFAULT_CHAT_FRAME:AddMessage(text)
end
---@return boolean
local function dumpTradeSkillFrame()
local name, curRank, maxRank = GetTradeSkillLine()
if name == nil then
return false
end
printLine(format("|cff40bf40Gromissingcrafts|r: dumping '%s' (rank %d/%d)", name, curRank or 0, maxRank or 0))
for i = 1, GetNumTradeSkills() do
local skillName, skillType = GetTradeSkillInfo(i)
if skillType ~= "header" then
local itemId = extractItemId(GetTradeSkillItemLink(i))
printLine(format(" [%d] %s -> item %s (%s)", i, skillName or "?", itemId and tostring(itemId) or "?", skillType or "?"))
end
end
printLine("Look each item id up on your realm's database to get the exact spell id and skill level, then add:")
printLine(" module:NewCraft(spellId, \"Recipe Name\", skillLevel, {SpellSource.Trainer}):SetResult(itemId):Save()")
return true
end
---@return boolean
local function dumpCraftFrame()
local name, curRank, maxRank = GetCraftDisplaySkillLine()
if name == nil then
return false
end
printLine(format("|cff40bf40Gromissingcrafts|r: dumping '%s' (rank %d/%d)", name, curRank or 0, maxRank or 0))
for i = 1, GetNumCrafts() do
local skillName, _, skillType = GetCraftInfo(i)
if skillType ~= "header" then
local itemId = extractItemId(GetCraftItemLink(i))
printLine(format(" [%d] %s -> item %s (%s)", i, skillName or "?", itemId and tostring(itemId) or "?", skillType or "?"))
end
end
return true
end
--[[
The trainer window lists every service the trainer offers, known or not -
unlike the trade skill window (which only ever shows recipes you've already
learned). No item link exists for a trainer entry though (you're learning a
spell, not previewing a crafted item), so this only gets names, not ids.
GetTrainerServiceInfo's exact return order isn't confirmed for this client,
so both candidate "type" slots are printed and checked for the header filter.
]]
---@return boolean
local function dumpTrainerFrame()
local count = GetNumTrainerServices()
if count == nil then
return false
end
printLine(format("|cff40bf40Gromissingcrafts|r: dumping trainer list (%d services)", count))
for i = 1, count do
local serviceName, b, c = GetTrainerServiceInfo(i)
if b ~= "header" and c ~= "header" then
printLine(format(" [%d] %s (%s / %s)", i, serviceName or "?", tostring(b), tostring(c)))
end
end
printLine("No item ids here - cross-reference these names against your realm's database, or check the trade skill window (item ids) after learning each one.")
return true
end
---@param frameName string
local function reportFrame(frameName)
local frame = getglobal(frameName)
if frame == nil then
printLine(format(" %s: does not exist on this client", frameName))
return
end
local ok, visible = pcall(function() return frame:IsVisible() end)
printLine(format(" %s: exists, IsVisible()=%s", frameName, ok and tostring(visible) or "error calling IsVisible"))
end
--[[
Nothing matched. Rather than guess again blind, report exactly what does and
doesn't exist on this client so the real cause (wrong frame name? visibility
check failing? something else entirely?) can be fixed instead of guessed at.
]]
local function diagnose()
printLine("|cffff8040Gromissingcrafts|r: no supported window detected. Diagnostics:")
reportFrame("TradeSkillFrame")
reportFrame("CraftFrame")
reportFrame("TrainerFrame")
reportFrame("ClassTrainerFrame")
local ok, numServices = pcall(GetNumTrainerServices)
if not ok then
printLine(" GetNumTrainerServices() = error calling it")
else
printLine(format(" GetNumTrainerServices() = %s", numServices == nil and "nil" or tostring(numServices)))
end
end
local function dump()
if getglobal("TradeSkillFrame") ~= nil and TradeSkillFrame:IsVisible() and dumpTradeSkillFrame() then
return
end
if getglobal("CraftFrame") ~= nil and CraftFrame:IsVisible() and dumpCraftFrame() then
return
end
if getglobal("TrainerFrame") ~= nil and TrainerFrame:IsVisible() and dumpTrainerFrame() then
return
end
-- This client names the trainer window ClassTrainerFrame (used for both
-- class and profession trainers), not the vanilla-standard TrainerFrame -
-- confirmed via /gmc dump's own diagnostics.
if getglobal("ClassTrainerFrame") ~= nil and ClassTrainerFrame:IsVisible() and dumpTrainerFrame() then
return
end
diagnose()
end
function DevTools:Create()
-- Must land in the REAL global table, not this addon's isolated
-- environment - the client's slash-command dispatcher scans real _G for
-- SLASH_* variables, and a bare assignment here would only ever set
-- Gromissingcrafts.SLASH_GROMISSINGCRAFTS1 (writes don't fall through
-- the environment's __index like reads do).
setglobal("SLASH_GROMISSINGCRAFTS1", "/gromissingcrafts")
setglobal("SLASH_GROMISSINGCRAFTS2", "/gmc")
SlashCmdList["GROMISSINGCRAFTS"] = function(msg)
local command = strlower(msg or "")
if command == "dump" then
dump()
else
printLine("|cff40bf40Gromissingcrafts|r: /gmc dump - print recipe names (+ item ids where available) from the open trade skill, craft, or trainer window")
end
end
end
+43
View File
@@ -0,0 +1,43 @@
--[[
Use the addon's private table as an isolated environment.
By using {__index = _G} metatable we're allowing all global lookups to transparently fallback to the game-wide
globals table, while the private table itself will act as a thin layer on top of the game-wide globals table,
allowing us to have our own global variables isolated from the rest of the game.
This accomplishes several goals:
1. Prevents addon-specific "globals" from leaking to game-wide global namespace _G
2. Optionally retains the ability to access these "globals" via the only exposed global variable "Gromissingcrafts"
3. Allows us to make overrides for WoW API global functions and variables without actually touching
the real global namespace, making these overrides visible only to this addon.
setfenv(1, Gromissingcrafts) must be added to every .lua file to allow it to work within this environment,
and this Environment file must be loaded before all others
]]
local _G = getfenv(0)
Gromissingcrafts = setmetatable({_G = _G}, {__index = _G})
setfenv(1, Gromissingcrafts)
ADDON_NAME = "Gromissingcrafts"
ADDON_VERSION = "1.0"
---@param t table
function erase(t)
setmetatable(t, nil)
for key, _ in pairs(t) do
t[key] = nil
end
table.setn(t, 0)
end
---@generic K
---@generic T
---@param t table<K, T>
---@param key K
---@return T|nil
function tpop(t, key)
local value = t[key]
t[key] = nil
return value
end
+84
View File
@@ -0,0 +1,84 @@
setfenv(1, Gromissingcrafts)
---@shape AddonInfo
---@field name string
---@field version string
---@type AddonInfo
local addonInfo = {
name = ADDON_NAME,
version = ADDON_VERSION
}
---@type LibStubDef
local LibStub = getglobal("LibStub")
assert(LibStub ~= nil, "LibStub is required to run this addon")
local LibCraftingProfessions = --[[---@type LibCraftingProfessions]] LibStub("LibCraftingProfessions-1.0")
local LibCrafts = --[[---@type LibCrafts]] LibStub("LibCrafts-1.0")
local LibItemTooltip = --[[---@type LibItemTooltip]] LibStub("LibItemTooltip-1.0")
---@shape Repositories
---@field characterRepository CharacterRepository
---@field craftRepository CraftRepository
---@class Gromissingcrafts
---@field database Database
---@field uiManager UIManager
---@type Gromissingcrafts
local addon = --[[---@type Gromissingcrafts]] {}
function addon:OnEnable()
self.database = Database:Create()
local characterRepository = CharacterRepository:Create(self.database)
---@type Repositories
local repositories = {
characterRepository = characterRepository,
craftRepository = CraftRepository:Create(self.database, characterRepository, LibCrafts)
}
-- L is the shared locale table populated by src/locale/*.lua, loaded earlier in the .toc
self.uiManager = UIManager:Create(addonInfo, repositories, --[[---@type GromissingcraftsLocale]] L, LibItemTooltip)
LibCraftingProfessions:RegisterEvent("LCP_SKILLS_UPDATE", function(profession, skills)
local skillNames = {}
for _, skill in ipairs(skills) do
tinsert(skillNames, skill.localized_name)
end
self.database:SaveCurrentPlayerSkills(profession.localized_name, profession.cur_rank, skillNames)
local playerProfessions = LibCraftingProfessions:GetPlayerProfessions()
if playerProfessions ~= nil then
local playerProfessionNames = {}
for _, playerProfession in ipairs(playerProfessions or {}) do
tinsert(playerProfessionNames, playerProfession.localized_name)
end
self.database:SaveCurrentPlayerProfessions(playerProfessionNames)
end
self.uiManager:OnDataUpdated(profession.localized_name)
end)
LibCraftingProfessions:RegisterEvent("LCP_FRAME_SHOW", function(profession, frame, frameType)
self.uiManager:OnProfessionFrameOpened(profession.localized_name, frame, frameType)
end)
LibCraftingProfessions:RegisterEvent("LCP_FRAME_CLOSE", function(frame, frameType)
self.uiManager:OnProfessionFrameClosed(frame, frameType)
end)
DevTools:Create()
end
Addon = addon
local bootstrapFrame = CreateFrame("Frame")
bootstrapFrame:RegisterEvent("ADDON_LOADED")
bootstrapFrame:SetScript("OnEvent", function()
if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
bootstrapFrame:UnregisterEvent("ADDON_LOADED")
addon:OnEnable()
end
end)
+68
View File
@@ -0,0 +1,68 @@
setfenv(1, Gromissingcrafts)
---@shape CharacterProfession
---@field localizedName string
---@field rank number
---@field knownLocalizedSkillNamesSet table<string, boolean>
---@class Character
---@field name string
---@field professionLocalizedNameToProfession table<string, CharacterProfession>
Character = {}
---@param dbCharacter DatabaseCharacter
---@return self
function Character:Create(dbCharacter)
---@type table<string, CharacterProfession>
local nameToProfession = {}
for professionName, profession in pairs(dbCharacter.professionsByLocalizedName) do
---@type table<string, boolean>
local skillSet = {}
for _, skillName in ipairs(profession.knownLocalizedSkillNames) do
skillSet[skillName] = true
end
nameToProfession[professionName] = {
localizedName = professionName,
rank = profession.rank,
knownLocalizedSkillNamesSet = skillSet,
}
end
local object = --[[---@type self]] {}
setmetatable(object, {__index = Character})
object.name = dbCharacter.name
object.professionLocalizedNameToProfession = nameToProfession
return object
end
---@param localizedName string
---@return number
function Character:GetProfessionRank(localizedName)
local profession = self.professionLocalizedNameToProfession[localizedName]
if profession == nil then
return 0
end
return profession.rank
end
---@param craft Craft
---@return boolean
function Character:Knows(craft)
local profession = self.professionLocalizedNameToProfession[craft.localizedProfessionName]
if profession == nil then
return false
end
return profession.knownLocalizedSkillNamesSet[craft.localizedName] == true
end
---@param craft Craft
---@return boolean
function Character:CanLearnNow(craft)
local profession = self.professionLocalizedNameToProfession[craft.localizedProfessionName]
if profession == nil then
return false
end
return not self:Knows(craft) and profession.rank >= craft.skillLevel
end
@@ -0,0 +1,36 @@
setfenv(1, Gromissingcrafts)
---@class CharacterRepository
---@field _db Database
CharacterRepository = {}
---@param database Database
---@return self
function CharacterRepository:Create(database)
self._db = database
return self
end
---@param exceptName ?string|nil
---@return Character[]
function CharacterRepository:FindAll(exceptName)
---@type Character[]
local characters = {}
for _, dbCharacter in ipairs(self._db:GetCharacters()) do
if dbCharacter.name ~= exceptName then
tinsert(characters, Character:Create(dbCharacter))
end
end
return characters
end
---@param name string
---@return Character|nil
function CharacterRepository:Find(name)
for _, dbCharacter in ipairs(self._db:GetCharacters()) do
if dbCharacter.name == name then
return Character:Create(dbCharacter)
end
end
return nil
end
+185
View File
@@ -0,0 +1,185 @@
setfenv(1, Gromissingcrafts)
---@class CraftRepository
---@field _db Database
---@field _characterRepository CharacterRepository
---@field _libCrafts LibCrafts
CraftRepository = {}
---@alias CraftSource "Chest" | "Craft" | "Drop" | "Fishing" | "Gift" | "Pickpocketing" | "Quest" | "Trainer" | "Vendor" | "WorldObject" | "Unknown"
---@type table<CraftSource, CraftSource>
CraftSource = {
Chest = "Chest",
Craft = "Craft",
Drop = "Drop",
Fishing = "Fishing",
Gift = "Gift",
Pickpocketing = "Pickpocketing",
Quest = "Quest",
Trainer = "Trainer",
Vendor = "Vendor",
WorldObject = "WorldObject",
Unknown = "Unknown",
}
---@shape Craft
---@field spellId number
---@field localizedProfessionName string
---@field localizedName string
---@field skillLevel number
---@field isAvailable boolean
---@field recipeId number|nil
---@field resultId number|nil
---@field sources CraftSource[]
---@param database Database
---@param LibCrafts LibCrafts
function CraftRepository:Create(database, characterRepository, LibCrafts)
self._db = database
self._characterRepository = characterRepository
self._libCrafts = LibCrafts
return self
end
---@type table<LcRecipeSource, CraftSource>
local RECIPE_SOURCE_TO_CRAFT_SOURCE = {}
---@type table<LcSpellSource, CraftSource>
local SPELL_SOURCE_TO_CRAFT_SOURCE = {}
---@param craft LcCraft
---@param LibCrafts LibCrafts
---@return CraftSource[]
local function parseSources(craft, LibCrafts)
if next(RECIPE_SOURCE_TO_CRAFT_SOURCE) == nil then
local RecipeSource = LibCrafts.constants.recipe_sources
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Chest] = CraftSource.Chest
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.CraftedByEngineer] = CraftSource.Craft
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Drop] = CraftSource.Drop
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Fishing] = CraftSource.Fishing
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.GiftedToReturningEngineers] = CraftSource.Gift
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Pickpocketing] = CraftSource.Pickpocketing
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Quest] = CraftSource.Quest
RECIPE_SOURCE_TO_CRAFT_SOURCE[RecipeSource.Vendor] = CraftSource.Vendor
end
if next(SPELL_SOURCE_TO_CRAFT_SOURCE) == nil then
local SpellSource = LibCrafts.constants.spell_sources
SPELL_SOURCE_TO_CRAFT_SOURCE[SpellSource.LearnedAutomatically] = CraftSource.Trainer
SPELL_SOURCE_TO_CRAFT_SOURCE[SpellSource.Quest] = CraftSource.Quest
SPELL_SOURCE_TO_CRAFT_SOURCE[SpellSource.Trainer] = CraftSource.Trainer
SPELL_SOURCE_TO_CRAFT_SOURCE[SpellSource.WorldObject] = CraftSource.WorldObject
end
---@type table<CraftSource, boolean>
local craftSourcesSet = {}
for _, recipe in ipairs(craft.recipes) do
for _, recipeSource in ipairs(recipe.sources) do
local craftSource = RECIPE_SOURCE_TO_CRAFT_SOURCE[recipeSource]
if craftSource ~= nil then
craftSourcesSet[craftSource] = true
end
end
end
for _, spellSource in ipairs(craft.sources) do
local craftSource = SPELL_SOURCE_TO_CRAFT_SOURCE[spellSource]
if craftSource ~= nil then
craftSourcesSet[craftSource] = true
end
end
---@type CraftSource[]
local craftSources = {}
for source, _ in pairs(craftSourcesSet) do
tinsert(craftSources, source)
end
return craftSources
end
---@param craft LcCraft
---@param professionRank number
---@param LibCrafts LibCrafts
---@return Craft
local function create(craft, professionRank, LibCrafts)
---@type number|nil
local recipeId
for _, recipe in ipairs(craft.recipes) do
recipeId = recipe.id
break
end
---@type number|nil
local resultId
if craft.result ~= nil then
resultId = (--[[---@not nil]] craft.result).id
end
return {
spellId = craft.spell_id,
localizedProfessionName = craft.localized_profession_name,
localizedName = craft.localized_spell_name,
skillLevel = craft.skill_level,
isAvailable = craft.skill_level <= professionRank,
recipeId = recipeId,
resultId = resultId,
sources = parseSources(craft, LibCrafts)
}
end
---@param characterName string
---@param localizedProfessionName string
---@param searchQuery string
---@return Craft[]
function CraftRepository:FindMissing(characterName, localizedProfessionName, searchQuery)
local professionRank = 0
local character = self._characterRepository:Find(characterName)
if character ~= nil then
professionRank = (--[[---@not nil]] character):GetProfessionRank(localizedProfessionName)
end
---@type table<string, boolean>
local characterSkillSet = {}
for _, skillName in ipairs(self._db:GetLocalizedSkillNames(characterName, localizedProfessionName)) do
characterSkillSet[skillName] = true
end
local lcSearchQuery = strlower(searchQuery)
---@type Craft[]
local crafts = {}
for _, craft in ipairs(self._libCrafts:GetCraftsByProfession(localizedProfessionName)) do
if characterSkillSet[craft.localized_spell_name] == nil then
local match = false
if lcSearchQuery ~= "" then
local lcSpellName = strlower(craft.localized_spell_name)
local a, b = strfind(lcSpellName, lcSearchQuery, 1, true)
match = a ~= nil and b ~= nil
else
match = true
end
if match then
tinsert(crafts, create(craft, professionRank, self._libCrafts))
end
end
end
return crafts
end
---@param itemId number
---@return Craft[]
function CraftRepository:FindByRecipeId(itemId)
local playerName, _ = UnitName("player")
local character = self._characterRepository:Find(playerName)
---@type Craft[]
local crafts = {}
for _, craft in ipairs(self._libCrafts:GetCraftsByRecipeId(itemId)) do
local professionRank = 0
if character ~= nil then
professionRank = (--[[---@not nil]] character):GetProfessionRank(craft.localized_profession_name)
end
tinsert(crafts, create(craft, professionRank, self._libCrafts))
end
return crafts
end
+146
View File
@@ -0,0 +1,146 @@
setfenv(1, Gromissingcrafts)
---@class Database
---@field _db DatabaseSavedVariable
Database = {}
---@shape DatabaseProfession
---@field rank number
---@field knownLocalizedSkillNames string[]
---@shape DatabaseCharacter
---@field realm string
---@field english_faction string
---@field localized_faction string
---@field name string
---@field professionsByLocalizedName table<string, DatabaseProfession>
---@shape DatabaseSavedVariable
---@field global {realmToNameToCharacter: table<string, table<string, DatabaseCharacter>>}
local SAVED_VARIABLE_NAME = "GromissingcraftsDatabase"
---@return DatabaseCharacter
local function newCharacterDefaults()
return {
realm = "",
english_faction = "",
localized_faction = "",
name = "",
professionsByLocalizedName = {},
}
end
---@return self
function Database:Create()
---@type DatabaseSavedVariable
local saved = getglobal(SAVED_VARIABLE_NAME)
if type(saved) ~= "table" then
saved = --[[---@type DatabaseSavedVariable]] {}
setglobal(SAVED_VARIABLE_NAME, saved)
end
if type(saved.global) ~= "table" then
saved.global = {realmToNameToCharacter = {}}
end
if type(saved.global.realmToNameToCharacter) ~= "table" then
saved.global.realmToNameToCharacter = {}
end
self._db = saved
return self
end
---@param realm string
---@return table<string, DatabaseCharacter>
function Database:_GetOrCreateRealm(realm)
local realms = self._db.global.realmToNameToCharacter
local nameToCharacter = realms[realm]
if nameToCharacter == nil then
nameToCharacter = {}
realms[realm] = nameToCharacter
end
return nameToCharacter
end
---@param realm string
---@param name string
---@return DatabaseCharacter
function Database:_GetOrCreateCharacter(realm, name)
local nameToCharacter = self:_GetOrCreateRealm(realm)
local character = nameToCharacter[name]
if character == nil then
character = newCharacterDefaults()
nameToCharacter[name] = character
end
return character
end
---@return DatabaseCharacter[]
function Database:GetCharacters()
self:_SavePlayer()
---@type DatabaseCharacter[]
local characters = {}
for _, nameToCharacter in pairs(self._db.global.realmToNameToCharacter) do
for _, character in pairs(nameToCharacter) do
tinsert(characters, character)
end
end
return characters
end
---@param characterName string
---@param localizedProfessionName string
---@return string[]
function Database:GetLocalizedSkillNames(characterName, localizedProfessionName)
---@type DatabaseProfession
local profession
for _, nameToCharacter in pairs(self._db.global.realmToNameToCharacter) do
if nameToCharacter[characterName] ~= nil then
profession = nameToCharacter[characterName].professionsByLocalizedName[localizedProfessionName]
end
end
return (profession or {}).knownLocalizedSkillNames or {}
end
---@param localizedProfessionName string
---@param professionRank number
---@param localizedSkillNames string[]
function Database:SaveCurrentPlayerSkills(localizedProfessionName, professionRank, localizedSkillNames)
local player = self:_SavePlayer()
player.professionsByLocalizedName[localizedProfessionName] = {
rank = professionRank,
knownLocalizedSkillNames = localizedSkillNames
}
end
---@param localizedProfessionNames string[]
function Database:SaveCurrentPlayerProfessions(localizedProfessionNames)
---@type table<string, boolean>
local set = {}
for _, localizedProfessionName in ipairs(localizedProfessionNames) do
set[localizedProfessionName] = true
end
local player = self:_SavePlayer()
for localizedProfessionName, _ in pairs(player.professionsByLocalizedName) do
if set[localizedProfessionName] == nil then
player.professionsByLocalizedName[localizedProfessionName] = nil
end
end
end
---@return DatabaseCharacter
function Database:_SavePlayer()
local realm = GetRealmName()
local name, _ = UnitName("player")
local english_faction, localized_faction = UnitFactionGroup("player")
local player = self:_GetOrCreateCharacter(realm, name)
player.realm = realm
player.english_faction = english_faction
player.localized_faction = localized_faction
player.name = name
return player
end
+44
View File
@@ -0,0 +1,44 @@
setfenv(1, Gromissingcrafts)
---@shape GromissingcraftsLocale
---@field recipe_tooltip_already_known string
---@field recipe_tooltip_can_learn_now string
---@field recipe_tooltip_can_learn_later string
---@field craft_tooltip_source string
---@field craft_source_chest string
---@field craft_source_craft string
---@field craft_source_drop string
---@field craft_source_fishing string
---@field craft_source_gift string
---@field craft_source_pickpocketing string
---@field craft_source_quest string
---@field craft_source_trainer string
---@field craft_source_unknown string
---@field craft_source_vendor string
---@field craft_source_world_object string
--[[
Minimal AceLocale-free replacement. enUS.lua must load first (see the .toc) and
always populates every key, acting as the fallback. Every other locale file only
receives the shared table (and thus only gets to override keys) when the running
client's GetLocale() matches its own code; otherwise NewLocale returns nil and the
file's `if L == nil then return end` guard skips it entirely.
]]
---@type GromissingcraftsLocale
L = L or --[[---@type GromissingcraftsLocale]] {}
---@alias LocaleCode "deDE" | "enUS" | "esES" | "frFR" | "koKR" | "ptBR" | "ruRU" | "zhCN" | "zhTW"
---@param code LocaleCode
---@param is_default boolean
---@return GromissingcraftsLocale|nil
function NewLocale(code, is_default)
if is_default then
return L
end
if GetLocale() == code then
return L
end
return nil
end
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("deDE", false)
if L == nil then return end
L.recipe_tooltip_already_known = "Bereits bekannt"
L.recipe_tooltip_can_learn_now = "Kann jetzt lernen"
L.recipe_tooltip_can_learn_later = "Kann später lernen"
L.craft_tooltip_source = "Quelle"
L.craft_source_chest = "Truhe"
L.craft_source_craft = "Handwerk"
L.craft_source_drop = "Drop"
L.craft_source_fishing = "Angeln"
L.craft_source_gift = "Geschenk"
L.craft_source_pickpocketing = "Taschendiebstahl"
L.craft_source_quest = "Quest"
L.craft_source_trainer = "Lehrer"
L.craft_source_unknown = "Unbekannt"
L.craft_source_vendor = "H\195\164ndler"
L.craft_source_world_object = "Objekt"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("enUS", true)
if L == nil then return end
L.recipe_tooltip_already_known = "Already known"
L.recipe_tooltip_can_learn_now = "Can learn now"
L.recipe_tooltip_can_learn_later = "Can learn later"
L.craft_tooltip_source = "Source"
L.craft_source_chest = "Chest"
L.craft_source_craft = "Craft"
L.craft_source_drop = "Drop"
L.craft_source_fishing = "Fishing"
L.craft_source_gift = "Gift"
L.craft_source_pickpocketing = "Pickpocketing"
L.craft_source_quest = "Quest"
L.craft_source_trainer = "Trainer"
L.craft_source_unknown = "Unknown"
L.craft_source_vendor = "Vendor"
L.craft_source_world_object = "Object"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("esES", false)
if L == nil then return end
L.recipe_tooltip_already_known = "Ya conoce"
L.recipe_tooltip_can_learn_now = "Puede aprender"
L.recipe_tooltip_can_learn_later = "Podrá aprender más tarde"
L.craft_tooltip_source = "Fuente"
L.craft_source_chest = "Cofre"
L.craft_source_craft = "Artesanía"
L.craft_source_drop = "Botín"
L.craft_source_fishing = "Pesca"
L.craft_source_gift = "Regalo"
L.craft_source_pickpocketing = "Carterista"
L.craft_source_quest = "Misión"
L.craft_source_trainer = "Instructor"
L.craft_source_unknown = "Desconocido"
L.craft_source_vendor = "Vendedor"
L.craft_source_world_object = "Objeto"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("frFR", false)
if L == nil then return end
L.recipe_tooltip_already_known = "Déjà connu"
L.recipe_tooltip_can_learn_now = "Peut apprendre"
L.recipe_tooltip_can_learn_later = "Pourra apprendre plus tard"
L.craft_tooltip_source = "Source"
L.craft_source_chest = "Boîte"
L.craft_source_craft = "Artisanat"
L.craft_source_drop = "Ramass\195\169"
L.craft_source_fishing = "Pêche"
L.craft_source_gift = "Cadeau"
L.craft_source_pickpocketing = "Vol à la tire"
L.craft_source_quest = "Qu\195\170te"
L.craft_source_trainer = "Instructeur"
L.craft_source_unknown = "Inconnu"
L.craft_source_vendor = "Marchand"
L.craft_source_world_object = "Objet"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("koKR", false)
if L == nil then return end
L.recipe_tooltip_already_known = "이미 알고 있음"
L.recipe_tooltip_can_learn_now = "지금 배울 수 있음"
L.recipe_tooltip_can_learn_later = "나중에 배울 수 있음"
L.craft_tooltip_source = "소스"
L.craft_source_chest = "상자"
L.craft_source_craft = "제작"
L.craft_source_drop = "드롭"
L.craft_source_fishing = "낙시"
L.craft_source_gift = "선물"
L.craft_source_pickpocketing = "소매치기"
L.craft_source_quest = "퀴스트"
L.craft_source_trainer = "훈련사"
L.craft_source_unknown = "알 수 없음"
L.craft_source_vendor = "상인"
L.craft_source_world_object = "오브젝트"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("ptBR", false)
if L == nil then return end
L.recipe_tooltip_already_known = "Já conhece"
L.recipe_tooltip_can_learn_now = "Pode aprender"
L.recipe_tooltip_can_learn_later = "Poderá aprender depois"
L.craft_tooltip_source = "Fonte"
L.craft_source_chest = "Baú"
L.craft_source_craft = "Artesanato"
L.craft_source_drop = "Drop"
L.craft_source_fishing = "Pesca"
L.craft_source_gift = "Presente"
L.craft_source_pickpocketing = "Furto"
L.craft_source_quest = "Missão"
L.craft_source_trainer = "Instrutor"
L.craft_source_unknown = "Desconhecido"
L.craft_source_vendor = "Vendedor"
L.craft_source_world_object = "Objeto"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("ruRU", false)
if L == nil then return end
L.recipe_tooltip_already_known = "Уже знает"
L.recipe_tooltip_can_learn_now = "Может изучить"
L.recipe_tooltip_can_learn_later = "Сможет изучить позже"
L.craft_tooltip_source = "Источник"
L.craft_source_chest = "Сундуки"
L.craft_source_craft = "Крафт"
L.craft_source_drop = "Дроп"
L.craft_source_fishing = "Рыбалка"
L.craft_source_gift = "Подарок"
L.craft_source_pickpocketing = "Воровство"
L.craft_source_quest = "Задание"
L.craft_source_trainer = "Тренер"
L.craft_source_unknown = "Неизвестно"
L.craft_source_vendor = "Продавец"
L.craft_source_world_object = "Объект"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("zhCN", false)
if L == nil then return end
L.recipe_tooltip_already_known = "已经学会"
L.recipe_tooltip_can_learn_now = "可以学习"
L.recipe_tooltip_can_learn_later = "以后可以学习"
L.craft_tooltip_source = "来源"
L.craft_source_chest = "宝箱"
L.craft_source_craft = "制作"
L.craft_source_drop = "掉落"
L.craft_source_fishing = "钓鱼"
L.craft_source_gift = "礼物"
L.craft_source_pickpocketing = "撓窃"
L.craft_source_quest = "任务"
L.craft_source_trainer = "训练师"
L.craft_source_unknown = "未知"
L.craft_source_vendor = "商人"
L.craft_source_world_object = "物体"
+21
View File
@@ -0,0 +1,21 @@
setfenv(1, Gromissingcrafts)
local L = --[[---@type GromissingcraftsLocale]] NewLocale("zhTW", false)
if L == nil then return end
L.recipe_tooltip_already_known = "已經學會"
L.recipe_tooltip_can_learn_now = "可以學習"
L.recipe_tooltip_can_learn_later = "以後可以學習"
L.craft_tooltip_source = "來源"
L.craft_source_chest = "寶箱"
L.craft_source_craft = "製作"
L.craft_source_drop = "掉落"
L.craft_source_fishing = "釣魚"
L.craft_source_gift = "禮物"
L.craft_source_pickpocketing = "撓窃"
L.craft_source_quest = "任務"
L.craft_source_trainer = "訓練師"
L.craft_source_unknown = "未知"
L.craft_source_vendor = "商人"
L.craft_source_world_object = "物體"
@@ -0,0 +1,58 @@
setfenv(1, Gromissingcrafts)
---@class DataStateManager
---@field _craftRepository CraftRepository
---@field _updateCrafts fun(crafts: Craft[]): void
---@field _selectedCharacter string|nil
---@field _selectedProfession string|nil
---@field _searchQuery string
DataStateManager = {}
---@param craftRepository CraftRepository
---@param updateCrafts fun(crafts: Craft[]): void
function DataStateManager:Create(craftRepository, updateCrafts)
self._craftRepository = craftRepository
self._updateCrafts = updateCrafts
return self
end
---@param profession string
function DataStateManager:OnWindowOpened(profession)
local player, _ = UnitName("player")
self._selectedCharacter = player
self._selectedProfession = profession
self._searchQuery = ""
self:UpdateCrafts()
end
function DataStateManager:OnWindowClosed()
self._selectedCharacter = nil
self._selectedProfession = nil
self._searchQuery = ""
end
---@param newProfession string
function DataStateManager:OnProfessionSwitchedInFrame(newProfession)
self._selectedProfession = newProfession
self:UpdateCrafts()
end
---@param profession string
function DataStateManager:OnCraftsUpdated(profession)
if profession == self._selectedProfession then
self:UpdateCrafts()
end
end
---@param filters Filters
function DataStateManager:OnFiltersChanged(filters)
self._searchQuery = filters.searchQuery
self:UpdateCrafts()
end
function DataStateManager:UpdateCrafts()
if self._selectedCharacter == nil or self._selectedProfession == nil then
return
end
self._updateCrafts(self._craftRepository:FindMissing(self._selectedCharacter, self._selectedProfession, self._searchQuery or ""))
end
+107
View File
@@ -0,0 +1,107 @@
setfenv(1, Gromissingcrafts)
---@type ScriptType[]
local SCRIPT_TYPES = {
"OnAnimFinished",
"OnChar",
"OnClick",
"OnColorSelect",
"OnCursorChanged",
"OnDoubleClick",
"OnDragStart",
"OnDragStop",
"OnEditFocusGained",
"OnEditFocusLost",
"OnEnter",
"OnEnterPressed",
"OnEscapePressed",
"OnEvent",
"OnHide",
"OnHorizontalScroll",
"OnHyperlinkClick",
"OnHyperlinkEnter",
"OnHyperlinkLeave",
"OnInputLanguageChanged",
"OnKeyDown",
"OnKeyUp",
"OnLeave",
"OnLoad",
"OnMessageScrollChanged",
"OnMouseDown",
"OnMouseUp",
"OnMouseWheel",
"OnReceiveDrag",
"OnScrollRangeChanged",
"OnShow",
"OnSizeChanged",
"OnSpacePressed",
"OnTabPressed",
"OnTextChanged",
"OnTextSet",
"OnTooltipAddMoney",
"OnTooltipCleared",
"OnTooltipSetDefaultAnchor",
"OnUpdate",
"OnUpdateModel",
"OnValueChanged",
"OnVerticalScroll",
}
---@param frame Frame
function clearFrame(frame)
for _, scriptType in ipairs(SCRIPT_TYPES) do
if frame:HasScript(scriptType) then
frame:SetScript(scriptType, nil)
end
end
frame:UnregisterAllEvents()
frame:ClearAllPoints()
frame:Hide()
frame:SetParent(nil)
end
---@param frame Frame
---@return string
function getFrameId(frame)
assert(type(frame) == "table")
local _, _, address = strfind(tostring(frame), "table: (%x+)")
assert(type(address) == "string")
return --[[---@type string]] address
end
---@param delayInFrames 0|1
---@param callback fun():void
function callDelayed(delayInFrames, callback)
if delayInFrames == 0 then
callback()
else
local delayFrame = CreateFrame("Frame")
local function onUpdate()
delayFrame:SetScript("OnUpdate", nil)
clearFrame(delayFrame)
callback()
end
delayFrame:SetScript("OnUpdate", onUpdate)
end
end
--[[
Vanilla 1.12 has no reliable native hooksecurefunc (that's a post-1.12 Blizzard
API tied to the secure-execution/taint system Turtle WoW's client may or may
not back-port). Replace the target global function with a wrapper that calls
our callback and then the original, same technique LibItemTooltip already
uses to hook GameTooltip methods.
]]
---@param funcName string
---@param callback fun(...): void
function hookGlobalFunction(funcName, callback)
local original = getglobal(funcName)
if type(original) ~= "function" then
return
end
setglobal(funcName, function()
callback(unpack(arg))
return original(unpack(arg))
end)
end
+149
View File
@@ -0,0 +1,149 @@
setfenv(1, Gromissingcrafts)
---@alias RecipeStatus "IsLearned" | "CanLearnNow" | "CanLearnLater" | "CannotLearn"
---@type table<RecipeStatus, RecipeStatus>
local RecipeStatus = {
IsLearned = "IsLearned",
CanLearnNow = "CanLearnNow",
CanLearnLater = "CanLearnLater",
CannotLearn = "CannotLearn",
}
---@shape TooltipItem
---@field characterName string
---@field characterProfessionRank number
---@field skillLevel number
---@field status RecipeStatus
---@class TooltipEnhancer
---@field _craftRepository CraftRepository
---@field _characterRepository CharacterRepository
---@field _locale GromissingcraftsLocale
TooltipEnhancer = {}
---@param craftRepository CraftRepository
---@param characterRepository CharacterRepository
---@param locale GromissingcraftsLocale
---@param LibItemTooltip LibItemTooltip
---@return self
function TooltipEnhancer:Create(craftRepository, characterRepository, locale, LibItemTooltip)
self._craftRepository = craftRepository
self._characterRepository = characterRepository
self._locale = locale
LibItemTooltip:RegisterEvent("OnShow", function(tooltip, itemLink, itemId)
if self:EnhanceTooltip(tooltip, itemId) then
tooltip:Show()
end
end)
return self
end
---@param tooltip GameTooltip
---@param itemId number
---@return boolean
function TooltipEnhancer:EnhanceTooltip(tooltip, itemId)
local items = self:CreateItems(itemId)
items = self:FilterItems(items)
if getn(items) == 0 then
return false
end
self:SortItems(items)
self:DrawItems(tooltip, items)
return true
end
---@param itemId number
---@return TooltipItem[]
function TooltipEnhancer:CreateItems(itemId)
local crafts = self._craftRepository:FindByRecipeId(itemId)
if getn(crafts) == 0 then
return {}
end
-- Assume that if single recipe teaches multiple spells that they are all have the same requirements.
local craft = crafts[1]
local items = {}
local player, _ = UnitName("player")
for _, character in ipairs(self._characterRepository:FindAll(player)) do
tinsert(items, {
characterName = character.name,
characterProfessionRank = character:GetProfessionRank(craft.localizedProfessionName),
skillLevel = craft.skillLevel,
status = self:GetRecipeStatus(character, craft),
})
end
return items
end
---@param items TooltipItem[]
---@return TooltipItem[]
function TooltipEnhancer:FilterItems(items)
local new_items = {}
for _, item in ipairs(items) do
if item.characterProfessionRank > 0 and item.status ~= RecipeStatus.CannotLearn then
tinsert(new_items, item)
end
end
return new_items
end
---@param items TooltipItem[]
function TooltipEnhancer:SortItems(items)
table.sort(items, function(a, b)
return a.characterName < b.characterName
end)
end
---@param tooltip GameTooltip
---@param items TooltipItem[]
function TooltipEnhancer:DrawItems(tooltip, items)
---@type table<RecipeStatus, string>
local statusToColor = {
[RecipeStatus.IsLearned] = "|cff808080",
[RecipeStatus.CanLearnNow] = "|cff40bf40",
[RecipeStatus.CanLearnLater] = "|cffff8040",
}
---@type table<RecipeStatus, string>
local statusToText = {
[RecipeStatus.IsLearned] = self._locale.recipe_tooltip_already_known,
[RecipeStatus.CanLearnNow] = self._locale.recipe_tooltip_can_learn_now,
[RecipeStatus.CanLearnLater] = self._locale.recipe_tooltip_can_learn_later,
}
tooltip:AddLine(" ")
for _, item in ipairs(items) do
local color = statusToColor[item.status]
local message = statusToText[item.status]
local leftText = format("%s%s|r", color, message)
local rightText = format("%s%s (%d)|r", color, item.characterName, item.characterProfessionRank)
tooltip:AddDoubleLine(leftText, rightText)
end
end
---@param character Character
---@param craft Craft
---@return RecipeStatus
function TooltipEnhancer:GetRecipeStatus(character, craft)
if character:Knows(craft) then
return RecipeStatus.IsLearned
end
if character:CanLearnNow(craft) then
return RecipeStatus.CanLearnNow
end
if character:GetProfessionRank(craft.localizedProfessionName) > 0 then
return RecipeStatus.CanLearnLater
end
return RecipeStatus.CannotLearn
end
+206
View File
@@ -0,0 +1,206 @@
setfenv(1, Gromissingcrafts)
---@shape ProfessionFrame
---@field id string
---@field type LcpProfessionFrameType
---@field profession string
---@field widget Frame
---@shape WindowState
---@field widget Window
---@field opened boolean
---@field frameId string|nil
---@class UIManager
---@field _addonInfo AddonInfo
---@field _repos Repositories
---@field _locale GromissingcraftsLocale
---@field _placementPolicy PlacementPolicy
---@field _vanillaFramePool VanillaFramePool
---@field _buttonsByFrameId table<string, OpenButton>
---@field _windowState WindowState
---@field _filtersPanel FiltersPanel
---@field _craftsList CraftsList
---@field _dataStateManager DataStateManager
UIManager = {}
---@param addonInfo AddonInfo
---@param repositories Repositories
---@param locale GromissingcraftsLocale
---@param LibItemTooltip LibItemTooltip
---@return self
function UIManager:Create(addonInfo, repositories, locale, LibItemTooltip)
self._addonInfo = addonInfo
self._repos = repositories
self._locale = locale
self._placementPolicy = PlacementPolicy
self._vanillaFramePool = VanillaFramePool:Create()
self._tooltipEnhancer = TooltipEnhancer:Create(self._repos.craftRepository, self._repos.characterRepository, locale, LibItemTooltip)
self._buttonsByFrameId = {}
---@param crafts Craft[]
local function updateCrafts(crafts)
if self._craftsList ~= nil then
self._craftsList:PopulateInterface(crafts)
end
end
self._dataStateManager = DataStateManager:Create(self._repos.craftRepository, updateCrafts)
hookGlobalFunction("MovePanelToLeft", function() self:UpdateGeometry() end)
hookGlobalFunction("MovePanelToCenter", function() self:UpdateGeometry() end)
return self
end
---@param profession string
function UIManager:OnDataUpdated(profession)
self._dataStateManager:OnCraftsUpdated(profession)
end
---@param profession string
---@param frame Frame
---@param frameType LcpProfessionFrameType
function UIManager:OnProfessionFrameOpened(profession, frame, frameType)
---@type ProfessionFrame
local professionFrame = {
id = getFrameId(frame),
type = frameType,
profession = profession,
widget = frame,
}
if self:IsWindowAttachedTo(professionFrame.id) then
self._dataStateManager:OnProfessionSwitchedInFrame(profession)
else
self:DestroyButton(professionFrame.id)
self:CreateButton(professionFrame)
end
self:UpdateGeometry()
end
---@param frame Frame
---@param frameType LcpProfessionFrameType
function UIManager:OnProfessionFrameClosed(frame, frameType)
local frameId = getFrameId(frame)
self:DestroyButton(frameId)
if self:IsWindowAttachedTo(frameId) then
self:CloseWindow()
else
self:UpdateGeometry()
end
end
---@param frame ProfessionFrame
function UIManager:OnWindowOpened(frame)
self._windowState.opened = true
self._windowState.frameId = frame.id
if self._filtersPanel ~= nil then
self._filtersPanel:Clear()
end
self._dataStateManager:OnWindowOpened(frame.profession)
self:CheckButtons()
end
function UIManager:OnWindowClosed()
self._windowState.opened = false
self._windowState.frameId = nil
self._dataStateManager:OnWindowClosed()
self:CheckButtons()
end
---@param frame ProfessionFrame
function UIManager:OnButtonClicked(frame)
if self:IsWindowAttachedTo(frame.id) then
self:CloseWindow()
else
self:OpenWindow(frame)
end
end
---@param professionFrame ProfessionFrame
function UIManager:CreateButton(professionFrame)
local FrameType = LibCraftingProfessionsConstants.FrameType
---@type 0|1
local delayInFrames = 0
if professionFrame.type == FrameType.AdvancedTradeSkillWindow2 or professionFrame.type == FrameType.Artisan then
delayInFrames = 1 -- Work around async window initialization behavior
end
local onClick = function()
self:OnButtonClicked(professionFrame)
end
callDelayed(delayInFrames, function()
self._buttonsByFrameId[professionFrame.id] = OpenButton:Create(onClick, professionFrame, self._placementPolicy)
end)
end
---@param frameId string
function UIManager:DestroyButton(frameId)
local button = tpop(self._buttonsByFrameId, frameId)
if button ~= nil then
(--[[---@not nil]] button):Destroy()
end
end
function UIManager:CheckButtons()
local activeId
if self._windowState ~= nil and self._windowState.opened then
activeId = self._windowState.frameId
end
for frameId, button in pairs(self._buttonsByFrameId) do
button:SetChecked(frameId == activeId)
end
end
---@param frame ProfessionFrame
function UIManager:OpenWindow(frame)
if self:IsWindowAttachedTo(frame.id) then
return
end
if self._windowState == nil then
local onClose = function()
self:OnWindowClosed()
end
local filtersPanel = FiltersPanel:Create()
local craftsList = CraftsList:Create(self._locale, self._vanillaFramePool)
local window = Window:Create(self._addonInfo, onClose, filtersPanel, craftsList, frame, self._placementPolicy)
filtersPanel:OnChange(function(filters)
self._dataStateManager:OnFiltersChanged(filters)
end)
self._windowState = {
widget = window,
frameId = frame.id,
opened = true
}
self._filtersPanel = filtersPanel
self._craftsList = craftsList
end
self._windowState.widget:Show(frame)
self:OnWindowOpened(frame)
end
function UIManager:CloseWindow()
self._windowState.widget:Hide()
self:OnWindowClosed()
end
---@param frameId string
---@return boolean
function UIManager:IsWindowAttachedTo(frameId)
return self._windowState ~= nil and self._windowState.opened and self._windowState.frameId == frameId
end
function UIManager:UpdateGeometry()
if self._windowState ~= nil and self._windowState.opened then
self._windowState.widget:UpdateGeometry()
end
end
@@ -0,0 +1,67 @@
setfenv(1, Gromissingcrafts)
---@class VanillaFramePool
---@field _framesByType table<string, Frame[]>
VanillaFramePool = {}
local RELEASED_FIELD = "__released_to_pool__"
---@return self
function VanillaFramePool:Create()
local object = {}
setmetatable(object, {__index = VanillaFramePool})
local result = --[[---@type self]] object
result._framesByType = {}
return result
end
function VanillaFramePool:Destroy()
for _, pool in pairs(self._framesByType or {}) do
erase(pool)
end
erase(self)
end
---@param frameType "Frame"
---@return Frame
---@overload fun(frameType: "Button"): Button
function VanillaFramePool:Acquire(frameType)
local pool = self._framesByType[frameType] or {}
local frame
if next(pool) == nil then
frame = CreateFrame(frameType, nil, nil, nil)
else
frame = tremove(pool)
local frameAsTable = --[[---@type table<string, any>]] frame
frameAsTable[RELEASED_FIELD] = false
end
return frame
end
---@param frame Frame
function VanillaFramePool:Release(frame)
local frameAsTable = --[[---@type table<string, any>]] frame
if frameAsTable[RELEASED_FIELD] then
return
end
local frameType = frame:GetFrameType()
local pool = self._framesByType[frameType]
if pool == nil then
pool = {}
self._framesByType[frameType] = pool
end
for _, child in ipairs({frame:GetChildren()}) do
self:Release(child)
end
clearFrame(frame)
frameAsTable[RELEASED_FIELD] = true
tinsert(pool, frame)
end
@@ -0,0 +1,75 @@
setfenv(1, Gromissingcrafts)
--[[
Search-only now - the profession/character dropdowns were removed. The
window is already scoped to whichever profession frame it's attached to and
always shows the current logged-in character, so nothing is lost: this panel
just holds the recipe-name search box.
]]
---@class FiltersPanel
---@field _frame Frame
---@field _searchField SearchField
---@field _onChange fun()
FiltersPanel = {}
---@shape Filters
---@field searchQuery string
local PANEL_WIDTH = 352
local PANEL_HEIGHT = 24
local SEARCH_FIELD_MARGIN = 8
FiltersPanel.WIDTH = PANEL_WIDTH
FiltersPanel.HEIGHT = PANEL_HEIGHT
---@return self
function FiltersPanel:Create()
local panel = --[[---@type self]] {}
setmetatable(panel, {__index = FiltersPanel})
local frame = CreateFrame("Frame", nil, nil)
frame:SetWidth(PANEL_WIDTH)
frame:SetHeight(PANEL_HEIGHT)
frame:SetFrameStrata("DIALOG")
local searchField = SearchField:Create(PANEL_WIDTH - (2 * SEARCH_FIELD_MARGIN), function()
if panel._onChange ~= nil then
panel._onChange()
end
end)
local searchFieldFrame = searchField:GetFrame()
searchFieldFrame:SetParent(frame)
searchFieldFrame:ClearAllPoints()
searchFieldFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", SEARCH_FIELD_MARGIN, 0)
searchFieldFrame:SetFrameStrata("DIALOG")
panel._frame = frame
panel._searchField = searchField
return panel
end
---@return Frame
function FiltersPanel:GetFrame()
return self._frame
end
---@param callback fun(filters: Filters)
function FiltersPanel:OnChange(callback)
self._onChange = function()
callback(self:GetFilters())
end
end
---@return Filters
function FiltersPanel:GetFilters()
return {
searchQuery = self._searchField:GetText(),
}
end
function FiltersPanel:Clear()
self._searchField:SetText("")
end
@@ -0,0 +1,77 @@
setfenv(1, Gromissingcrafts)
--[[
Not InputBoxTemplate - same reasoning as Dropdown.lua: pfUI (and similar
skinning addons) appear to hook known Blizzard template names and that broke
click/focus handling for other widgets here. A bare EditBox with a manual
backdrop avoids giving it anything to key off of.
]]
---@class SearchField
---@field _frame EditBox
---@field _text string
SearchField = {}
---@param width number pixel width
---@param onChange fun()
---@return self
function SearchField:Create(width, onChange)
local object = --[[---@type self]] {}
setmetatable(object, {__index = SearchField})
local frame = CreateFrame("EditBox", nil, nil)
frame:SetWidth(width)
frame:SetHeight(20)
frame:SetFrameStrata("DIALOG")
frame:SetAutoFocus(false)
frame:SetMaxLetters(250)
frame:EnableMouse(true)
frame:SetBackdrop({
bgFile = [[Interface\Tooltips\UI-Tooltip-Background]],
edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]],
tile = true,
tileSize = 16,
edgeSize = 12,
insets = {left = 3, right = 3, top = 3, bottom = 3},
})
frame:SetBackdropColor(0, 0, 0, 0.8)
frame:SetFontObject(GameFontHighlightSmall)
frame:SetTextInsets(6, 6, 0, 0)
frame:SetJustifyH("LEFT")
frame:SetScript("OnTextChanged", function()
object._text = frame:GetText()
onChange()
end)
frame:SetScript("OnEscapePressed", function()
frame:ClearFocus()
end)
frame:SetScript("OnEnterPressed", function()
frame:ClearFocus()
end)
frame:SetScript("OnMouseDown", function()
frame:SetFocus()
end)
object._frame = frame
object._text = ""
return object
end
---@return Frame
function SearchField:GetFrame()
return self._frame
end
---@return string
function SearchField:GetText()
return self._text
end
---@param text string
function SearchField:SetText(text)
self._frame:SetText(text)
self._text = text
end
+282
View File
@@ -0,0 +1,282 @@
setfenv(1, Gromissingcrafts)
--[[
Fixed pool of row widgets sized to exactly fill the visible area (see the
earlier note in git history / CHANGES about why - a real ScrollFrame wasn't
clipping content reliably on this client). Scrolling changes which slice of
the data those rows display; the scrollbar here is a fully custom, drag/click
capable widget built the same template-free way as Dropdown.lua, and every
piece gets an explicit "DIALOG" strata - strata is NOT inherited from a
parent frame in this client, so anything left unset can lose input priority
to whatever else is on screen at that spot.
]]
---@class CraftsList
---@field _frame Frame
---@field _rows Frame
---@field _track Frame
---@field _thumb Button
---@field _locale GromissingcraftsLocale
---@field _framePool VanillaFramePool
---@field _items CraftsListItem[]
---@field _crafts Craft[]
---@field _scrollOffset number
---@field _visibleRowCount number
CraftsList = {}
local CONTENT_WIDTH = 352
local SCROLLBAR_WIDTH = 24 -- 1.5x the original 16px
local SCROLLBAR_GAP = 4
local ROW_WIDTH = CONTENT_WIDTH - SCROLLBAR_WIDTH - SCROLLBAR_GAP
local ROW_HEIGHT = 16
---@return number
local function getCursorY()
local _, y = GetCursorPosition()
return y / UIParent:GetEffectiveScale()
end
---@param locale GromissingcraftsLocale
---@param vanillaFramePool VanillaFramePool
---@return CraftsList
function CraftsList:Create(locale, vanillaFramePool)
local list = --[[---@type self]] {}
setmetatable(list, {__index = CraftsList})
local frame = CreateFrame("Frame", nil, nil)
frame:SetWidth(CONTENT_WIDTH)
frame:SetFrameStrata("DIALOG")
frame:EnableMouseWheel(true)
frame:SetScript("OnMouseWheel", function()
list:Scroll(-arg1)
end)
local rows = CreateFrame("Frame", nil, frame)
rows:SetWidth(ROW_WIDTH)
rows:SetFrameStrata("DIALOG")
rows:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
local upButton = CreateFrame("Button", nil, frame)
upButton:SetWidth(SCROLLBAR_WIDTH)
upButton:SetHeight(SCROLLBAR_WIDTH)
upButton:SetFrameStrata("DIALOG")
upButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
local upTexture = upButton:CreateTexture()
upTexture:SetTexture([[Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up]])
upTexture:SetAllPoints(upButton)
upButton:SetNormalTexture(upTexture)
local upHighlight = upButton:CreateTexture()
upHighlight:SetTexture([[Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight]])
upHighlight:SetAllPoints(upButton)
upHighlight:SetBlendMode("ADD")
upButton:SetHighlightTexture(upHighlight)
upButton:SetScript("OnClick", function() list:Scroll(-1) end)
local downButton = CreateFrame("Button", nil, frame)
downButton:SetWidth(SCROLLBAR_WIDTH)
downButton:SetHeight(SCROLLBAR_WIDTH)
downButton:SetFrameStrata("DIALOG")
downButton:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
local downTexture = downButton:CreateTexture()
downTexture:SetTexture([[Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up]])
downTexture:SetAllPoints(downButton)
downButton:SetNormalTexture(downTexture)
local downHighlight = downButton:CreateTexture()
downHighlight:SetTexture([[Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight]])
downHighlight:SetAllPoints(downButton)
downHighlight:SetBlendMode("ADD")
downButton:SetHighlightTexture(downHighlight)
downButton:SetScript("OnClick", function() list:Scroll(1) end)
local track = CreateFrame("Frame", nil, frame)
track:SetWidth(SCROLLBAR_WIDTH)
track:SetFrameStrata("DIALOG")
track:SetPoint("TOP", upButton, "BOTTOM", 0, 0)
track:SetPoint("BOTTOM", downButton, "TOP", 0, 0)
local trackTexture = track:CreateTexture()
trackTexture:SetTexture([[Interface\Buttons\UI-ScrollBar-Track]])
trackTexture:SetAllPoints(track)
local thumb = CreateFrame("Button", nil, track)
thumb:SetWidth(SCROLLBAR_WIDTH - 2)
thumb:SetFrameStrata("DIALOG")
thumb:EnableMouse(true)
local thumbTexture = thumb:CreateTexture()
thumbTexture:SetTexture([[Interface\Buttons\UI-ScrollBar-Knob]])
thumbTexture:SetAllPoints(thumb)
thumb:SetNormalTexture(thumbTexture)
thumb:Hide()
list._frame = frame
list._rows = rows
list._track = track
list._thumb = thumb
list._locale = locale
list._framePool = vanillaFramePool
list._items = {}
list._crafts = {}
list._scrollOffset = 0
list._visibleRowCount = 0
thumb:SetScript("OnMouseDown", function()
thumb:SetScript("OnUpdate", function()
if not IsMouseButtonDown("LeftButton") then
thumb:SetScript("OnUpdate", nil)
return
end
list:_DragThumbTo(getCursorY())
end)
end)
thumb:SetScript("OnMouseUp", function()
thumb:SetScript("OnUpdate", nil)
end)
return list
end
---@return Frame
function CraftsList:GetFrame()
return self._frame
end
---@param height number
function CraftsList:SetHeight(height)
self._frame:SetHeight(height)
self._rows:SetHeight(height)
self._visibleRowCount = math.max(1, math.floor(height / ROW_HEIGHT))
self:_EnsureItemPool()
self:_Render()
end
function CraftsList:_EnsureItemPool()
---@type Frame
local anchor = self._rows
for i = 1, self._visibleRowCount do
if self._items[i] == nil then
local item = CraftsListItem:Create(ROW_WIDTH, ROW_HEIGHT, self._locale, self._framePool)
item:GetFrame():SetFrameStrata("DIALOG")
item:Attach(self._rows, anchor, "TOPLEFT", i == 1 and "TOPLEFT" or "BOTTOMLEFT", 0, 0)
self._items[i] = item
end
anchor = self._items[i]:GetFrame()
end
for i = getn(self._items), self._visibleRowCount + 1, -1 do
local item = tremove(self._items, i)
item:Destroy()
end
end
---@param delta number
function CraftsList:Scroll(delta)
self._scrollOffset = self._scrollOffset + delta
self:_Render()
end
---@param cursorY number
function CraftsList:_DragThumbTo(cursorY)
local maxOffset = getn(self._crafts) - self._visibleRowCount
if maxOffset <= 0 then
return
end
local trackTop = self._track:GetTop()
local trackHeight = self._track:GetHeight()
local thumbHeight = self._thumb:GetHeight()
local travel = trackHeight - thumbHeight
if travel <= 0 then
return
end
local distanceFromTop = trackTop - cursorY - (thumbHeight / 2)
local progress = distanceFromTop / travel
if progress < 0 then
progress = 0
elseif progress > 1 then
progress = 1
end
self._scrollOffset = math.floor((progress * maxOffset) + 0.5)
self:_Render()
end
---@param a Craft
---@param b Craft
---@return boolean
local function compareCrafts(a, b)
if a.skillLevel ~= b.skillLevel then
return a.skillLevel < b.skillLevel
end
return a.localizedName < b.localizedName
end
---@param crafts Craft[]
function CraftsList:PopulateInterface(crafts)
table.sort(crafts, compareCrafts)
self._crafts = crafts
self._scrollOffset = 0
self:_Render()
end
function CraftsList:_Render()
local craftCount = getn(self._crafts)
local maxOffset = craftCount - self._visibleRowCount
if maxOffset < 0 then
maxOffset = 0
end
if self._scrollOffset > maxOffset then
self._scrollOffset = maxOffset
end
if self._scrollOffset < 0 then
self._scrollOffset = 0
end
---@param clickedItem CraftsListItem
local highlightItem = function(clickedItem)
for _, otherItem in ipairs(self._items) do
otherItem:SetHighlight(otherItem == clickedItem)
end
end
for i = 1, self._visibleRowCount do
local item = self._items[i]
if item ~= nil then
local craft = self._crafts[self._scrollOffset + i]
if craft ~= nil then
item:SetHighlight(false)
item:OnClick(highlightItem)
item:PopulateInterface(craft)
item:GetFrame():Show()
else
item:GetFrame():Hide()
end
end
end
self:_UpdateThumb(maxOffset, craftCount)
end
---@param maxOffset number
---@param craftCount number
function CraftsList:_UpdateThumb(maxOffset, craftCount)
if maxOffset <= 0 then
self._thumb:Hide()
return
end
local trackHeight = self._track:GetHeight()
local thumbHeight = trackHeight * (self._visibleRowCount / craftCount)
if thumbHeight < 10 then
thumbHeight = 10
end
if thumbHeight > trackHeight then
thumbHeight = trackHeight
end
local travel = trackHeight - thumbHeight
local progress = self._scrollOffset / maxOffset
self._thumb:SetHeight(thumbHeight)
self._thumb:ClearAllPoints()
self._thumb:SetPoint("TOP", self._track, "TOP", 0, -(progress * travel))
self._thumb:Show()
end
@@ -0,0 +1,156 @@
setfenv(1, Gromissingcrafts)
---@class CraftsListItem
---@field _locale GromissingcraftsLocale
---@field _framePool VanillaFramePool
---@field _button Button
---@field _fontString FontString
---@field _craft Craft
CraftsListItem = {}
---@type table<CraftSource, string>
local SOURCE_TO_TEXT = {}
---@param width number
---@param height number
---@param locale GromissingcraftsLocale
---@param framePool VanillaFramePool
---@return self
function CraftsListItem:Create(width, height, locale, framePool)
local button = framePool:Acquire("Button")
button:SetWidth(width)
button:SetHeight(height)
button:SetTextFontObject(GameFontNormal)
button:SetHighlightFontObject(GameFontHighlight)
button:SetText("")
local fontString = button:GetFontString()
fontString:SetPoint("TOPLEFT", 0, 0)
fontString:SetJustifyH("LEFT")
if next(SOURCE_TO_TEXT) == nil then
SOURCE_TO_TEXT[CraftSource.Chest] = locale.craft_source_chest
SOURCE_TO_TEXT[CraftSource.Craft] = locale.craft_source_craft
SOURCE_TO_TEXT[CraftSource.Drop] = locale.craft_source_drop
SOURCE_TO_TEXT[CraftSource.Fishing] = locale.craft_source_fishing
SOURCE_TO_TEXT[CraftSource.Gift] = locale.craft_source_gift
SOURCE_TO_TEXT[CraftSource.Pickpocketing] = locale.craft_source_pickpocketing
SOURCE_TO_TEXT[CraftSource.Quest] = locale.craft_source_quest
SOURCE_TO_TEXT[CraftSource.Trainer] = locale.craft_source_trainer
SOURCE_TO_TEXT[CraftSource.Vendor] = locale.craft_source_vendor
SOURCE_TO_TEXT[CraftSource.WorldObject] = locale.craft_source_world_object
SOURCE_TO_TEXT[CraftSource.Unknown] = locale.craft_source_unknown
end
local object = {}
setmetatable(object, {__index = CraftsListItem})
local result = --[[---@type self]] object
result._locale = locale
result._framePool = framePool
result._button = button
result._fontString = fontString
button:SetScript("OnEnter", function()
GameTooltip:SetOwner(button, "ANCHOR_TOPRIGHT")
if result:_DrawTooltip(GameTooltip) then
GameTooltip:Show()
end
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
return result
end
function CraftsListItem:Destroy()
if GameTooltip:IsOwned(self._button) then
GameTooltip:Hide()
end
self._framePool:Release(self._button)
erase(self)
end
---@param callback fun(item: CraftsListItem)
function CraftsListItem:OnClick(callback)
self._button:SetScript("OnClick", function() callback(self) end)
end
---@return Frame
function CraftsListItem:GetFrame()
return self._button
end
---@param parent Frame
---@param anchor Frame
---@param selfAnchorPoint WidgetAnchorPoint
---@param foreignAnchorPoint WidgetAnchorPoint
---@param x number
---@param y number
function CraftsListItem:Attach(parent, anchor, selfAnchorPoint, foreignAnchorPoint, x, y)
self._button:SetParent(parent)
self._button:ClearAllPoints()
self._button:SetPoint(selfAnchorPoint, anchor, foreignAnchorPoint, x, y)
self._button:Show()
end
---@param craft Craft
function CraftsListItem:PopulateInterface(craft)
local color = craft.isAvailable and {r = 0.7, g = 0.9, b = 1.0} or {r = 1.0, g = 0.8, b = 0.9}
local text = format("[%d] %s", craft.skillLevel, craft.localizedName)
self._button:SetText(text)
self._button:SetTextColor(color.r, color.g, color.b)
self._fontString:SetText(text)
self._craft = craft
end
---@param enable boolean
function CraftsListItem:SetHighlight(enable)
if enable then
self._button:LockHighlight()
else
self._button:UnlockHighlight()
end
end
---@param tooltip GameTooltip
---@return boolean
function CraftsListItem:_DrawTooltip(tooltip)
if self._craft == nil then
return false
end
if self._craft.recipeId ~= nil then
tooltip:SetHyperlink(format("item:%d", self._craft.recipeId))
elseif self._craft.resultId ~= nil then
tooltip:SetHyperlink(format("item:%d", self._craft.resultId))
else
tooltip:SetHyperlink(GetSpellLink(self._craft.spellId))
end
tooltip:AddLine(" ")
local strings = {}
for _, source in ipairs(self._craft.sources) do
local name = SOURCE_TO_TEXT[source]
if name ~= nil then
tinsert(strings, name)
end
end
if getn(strings) == 0 then
tinsert(strings, SOURCE_TO_TEXT[CraftSource.Unknown])
end
table.sort(strings)
local count = getn(strings)
local text = ""
for i, s in ipairs(strings) do
text = text .. s .. (i < count and ", " or "")
end
tooltip:AddLine(format("%s: %s", self._locale.craft_tooltip_source, text))
return true
end
@@ -0,0 +1,58 @@
setfenv(1, Gromissingcrafts)
---@class OpenButton
---@field _frame CheckButton
OpenButton = {}
---@param onClick fun():void
---@param professionFrame ProfessionFrame
---@param placementPolicy PlacementPolicy
function OpenButton:Create(onClick, professionFrame, placementPolicy)
local object = --[[---@type self]] {}
setmetatable(object, {__index = OpenButton})
local button = CreateFrame("CheckButton")
button:SetWidth(32)
button:SetHeight(32)
local normalTexture = button:CreateTexture()
normalTexture:SetTexture([[Interface\Icons\INV_Scroll_05]])
normalTexture:SetAllPoints(button)
button:SetNormalTexture(normalTexture)
local highlightTexture = button:CreateTexture()
highlightTexture:SetTexture([[Interface\Buttons\ButtonHilight-Square]])
highlightTexture:SetAllPoints(button)
highlightTexture:SetBlendMode("ADD")
button:SetHighlightTexture(highlightTexture)
local checkedTexture = button:CreateTexture()
checkedTexture:SetTexture([[Interface\Buttons\CheckButtonHilight]])
checkedTexture:SetAllPoints(button)
checkedTexture:SetBlendMode("ADD")
button:SetCheckedTexture(checkedTexture)
local anchor = placementPolicy:GetOpenButtonAnchor(professionFrame.type)
button:SetParent(professionFrame.widget)
button:SetPoint(anchor.framePoint, professionFrame.widget, anchor.selfPoint, anchor.selfCoords.x, anchor.selfCoords.y)
button:SetScript("OnClick", function()
onClick()
end)
button:Show()
object._frame = button
return object
end
function OpenButton:Destroy()
clearFrame(self._frame)
self._frame = nil
erase(self)
end
---@param checked boolean
function OpenButton:SetChecked(checked)
self._frame:SetChecked(checked)
end
@@ -0,0 +1,106 @@
setfenv(1, Gromissingcrafts)
---@shape Coords
---@field x number
---@field y number
---@shape Anchor
---@field framePoint WidgetAnchorPoint
---@field selfPoint WidgetAnchorPoint
---@field selfCoords Coords
---@shape Geometry
---@field width number
---@field height number
---@field offsetX number
---@field offsetY number
---@class PlacementPolicy
PlacementPolicy = {}
local FrameType = LibCraftingProfessionsConstants.FrameType
---@return boolean
local function pfUI()
return IsAddOnLoaded("pfUI") == 1
end
---@param frameType LcpProfessionFrameType
---@return boolean
local function frameTypeSupportedByPfUI(frameType)
return frameType == FrameType.VanillaCraftFrame or
frameType == FrameType.VanillaTradeSkillFrame or
frameType == FrameType.TurtleTradeSkillFrame
end
---@return boolean
local function MTSL()
return getglobal("MTSLUI_TOGGLE_BUTTON") ~= nil
end
---@param frameType LcpProfessionFrameType
---@return Anchor
function PlacementPolicy:GetOpenButtonAnchor(frameType)
---@type Anchor
local anchor = {
framePoint = "TOPRIGHT",
selfPoint = "TOPRIGHT",
selfCoords = {x = 0, y = 0},
}
if pfUI() and frameTypeSupportedByPfUI(frameType) then
if MTSL() then
anchor.selfCoords = {x = -15, y = -70}
else
anchor.selfCoords = {x = -30, y = -1}
end
elseif frameType == FrameType.AdvancedTradeSkillWindow then
anchor.selfCoords = {x = -54, y = -91}
elseif frameType == FrameType.AdvancedTradeSkillWindow2 then
anchor.selfCoords = {x = -48, y = -77}
elseif frameType == FrameType.Artisan then
anchor.selfCoords = {x = -44, y = -60}
elseif frameType == FrameType.TurtleTradeSkillFrame then
anchor.selfCoords = {x = -96, y = -61}
elseif frameType == FrameType.VanillaTradeSkillFrame then
if MTSL() then
anchor.selfCoords = {x = -93, y = 20}
else
anchor.selfCoords = {x = -38, y = 20}
end
elseif frameType == FrameType.VanillaCraftFrame then
anchor.selfCoords = {x = -44, y = -60}
end
return anchor
end
--[[
offsetX/offsetY are relative to the profession frame's TOPRIGHT corner, meant
to be used directly as a SetPoint offset anchored to that frame (not to
UIParent). That makes the window a true child-of-position: WoW keeps it in
sync automatically whenever the profession frame moves, with no polling or
move hooks required.
]]
---@param frameType LcpProfessionFrameType
---@return Geometry
function PlacementPolicy:GetMainWindowGeometry(frameType)
---@type Geometry
local status
if pfUI() and frameTypeSupportedByPfUI(frameType) then
status = {width = 384, height = 450, offsetX = 0, offsetY = 5}
elseif frameType == FrameType.AdvancedTradeSkillWindow then
status = {width = 384, height = 430, offsetX = -40, offsetY = -10}
elseif frameType == FrameType.AdvancedTradeSkillWindow2 then
status = {width = 384, height = 494, offsetX = -5, offsetY = -10}
elseif frameType == FrameType.Artisan then
status = {width = 384, height = 469, offsetX = -5, offsetY = -10}
elseif frameType == FrameType.TurtleTradeSkillFrame then
status = {width = 384, height = 430, offsetX = -87, offsetY = -10}
else
status = {width = 384, height = 430, offsetX = -40, offsetY = -10}
end
return status
end
+110
View File
@@ -0,0 +1,110 @@
setfenv(1, Gromissingcrafts)
---@class Window
---@field _frame Frame
---@field _titleText FontString
---@field _professionFrame ProfessionFrame
---@field _closeButton Button
---@field _placementPolicy PlacementPolicy
---@field _onClose function
Window = {}
local BACKDROP = {
bgFile = [[Interface\DialogFrame\UI-DialogBox-Background]],
edgeFile = [[Interface\DialogFrame\UI-DialogBox-Border]],
tile = true,
tileSize = 32,
edgeSize = 32,
insets = {left = 11, right = 12, top = 12, bottom = 11},
}
local TITLE_BAR_HEIGHT = 24
local CONTENT_MARGIN = 16
---@param addonInfo AddonInfo
---@param onClose fun():void
---@param filtersPanel FiltersPanel
---@param craftsList CraftsList
---@param professionFrame ProfessionFrame
---@param placementPolicy PlacementPolicy
---@return self
function Window:Create(addonInfo, onClose, filtersPanel, craftsList, professionFrame, placementPolicy)
local window = --[[---@type self]] {}
setmetatable(window, {__index = Window})
local frameStatus = placementPolicy:GetMainWindowGeometry(professionFrame.type)
local frame = CreateFrame("Frame", "GromissingcraftsWindow", UIParent)
frame:SetBackdrop(BACKDROP)
frame:SetBackdropColor(1, 1, 1, 1)
-- Fixed high strata (not "whatever the profession frame uses") so this
-- window and its widgets reliably win input priority over anything the
-- profession frame or other addons draw underneath it.
frame:SetFrameStrata("DIALOG")
frame:SetToplevel(true)
frame:SetWidth(frameStatus.width)
frame:SetHeight(frameStatus.height)
-- Anchored directly to the profession frame (not to a computed UIParent
-- offset) so it moves together with it automatically - no drag support
-- of its own, it's not meant to be repositioned independently.
frame:SetPoint("TOPLEFT", professionFrame.widget, "TOPRIGHT", frameStatus.offsetX, frameStatus.offsetY)
local titleText = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
titleText:SetPoint("TOP", frame, "TOP", 0, -14)
titleText:SetText(format('%s v%s', addonInfo.name, addonInfo.version))
local closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
closeButton:SetFrameStrata("DIALOG")
closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)
closeButton:SetScript("OnClick", function()
window:Hide()
end)
local filtersPanelFrame = filtersPanel:GetFrame()
filtersPanelFrame:SetParent(frame)
filtersPanelFrame:ClearAllPoints()
filtersPanelFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", CONTENT_MARGIN, -(TITLE_BAR_HEIGHT + 8))
local craftsListFrame = craftsList:GetFrame()
craftsListFrame:SetParent(frame)
craftsListFrame:ClearAllPoints()
craftsListFrame:SetPoint("TOPLEFT", filtersPanelFrame, "BOTTOMLEFT", 0, -5)
window._frame = frame
window._titleText = titleText
window._professionFrame = professionFrame
window._closeButton = closeButton
window._placementPolicy = placementPolicy
window._craftsList = craftsList
window._onClose = onClose
window:UpdateGeometry()
return window
end
function Window:UpdateGeometry()
local frameStatus = self._placementPolicy:GetMainWindowGeometry(self._professionFrame.type)
self._frame:SetWidth(frameStatus.width)
self._frame:SetHeight(frameStatus.height)
self._frame:ClearAllPoints()
self._frame:SetPoint("TOPLEFT", self._professionFrame.widget, "TOPRIGHT", frameStatus.offsetX, frameStatus.offsetY)
self._frame:Show()
-- Recomputed on every geometry change, not just at creation: different
-- profession frame types (and pfUI vs not) report different heights.
self._craftsList:SetHeight(frameStatus.height - (TITLE_BAR_HEIGHT + 8) - FiltersPanel.HEIGHT - CONTENT_MARGIN)
end
---@param professionFrame ProfessionFrame
function Window:Show(professionFrame)
self._professionFrame = professionFrame
self._frame:Show()
self:UpdateGeometry()
end
function Window:Hide()
self._frame:Hide()
self._onClose()
end