aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/pharmacy.md
diff options
context:
space:
mode:
Diffstat (limited to 'skill_plans/pharmacy.md')
-rw-r--r--skill_plans/pharmacy.md2421
1 files changed, 2421 insertions, 0 deletions
diff --git a/skill_plans/pharmacy.md b/skill_plans/pharmacy.md
new file mode 100644
index 0000000..5e0b7af
--- /dev/null
+++ b/skill_plans/pharmacy.md
@@ -0,0 +1,2421 @@
+# Pharmacy Skill Implementation Plan
+
+## 1. Overview
+
+Pharmacy replaces the Alchemy skill everywhere in the codebase. It is a production skill centered on creating potions from herbs and reagents.
+
+**Production chain:**
+1. **Obtain grimy herbs** -- mob drops, farming (future), or ground spawns
+2. **Clean herbs** (`clean` command) -- background action, awards Pharmacy XP, level-gated per herb
+3. **Create unfinished potions** (`use` command) -- combine clean herb + vial of water via MadeFrom (no XP, no level req)
+4. **Finish potions** (`mix` command) -- combine unfinished potion + reagent via recipe system, awards Pharmacy XP, level-gated
+
+**Potion effects are NOT implemented yet.** Potions are items only. A future update will add consumable effects (stat boosts, healing, curing). For now, potions have `description` text describing their intended effect and a `value` for trading.
+
+---
+
+## 2. Skill Rename (Alchemy -> Pharmacy)
+
+### Files to change
+
+**`internal/player/player.go`** -- 3 locations:
+- Line 24: `Alchemy SkillName = "alchemy"` -> `Pharmacy SkillName = "pharmacy"`
+- Line 37: `Alchemy, Thieving,` -> `Pharmacy, Thieving,`
+- Line 56: `Alchemy: "alc",` -> `Pharmacy: "pha",`
+
+**`data/help/` YAML files** -- if any reference "alchemy" in text, update to "pharmacy". Currently none exist for alchemy, so no changes needed. New help files will be created.
+
+**`AGENTS.md`** -- Update the Skills table:
+- Change `Alchemy` to `Pharmacy` in the Production skills row
+- Update the `SkillAbbr` reference from `"alc"` to `"pha"`
+
+**No other Go files reference "alchemy" or "Alchemy" directly** (confirmed by grep). The skill constant `Alchemy` is only used in `player.go` in 3 places.
+
+**Existing player save files** -- Any character YAML files with `alchemy: <xp>` in their skills map will need the key renamed to `pharmacy`. Add a migration note: on load, if `skills` map contains key `"alchemy"`, copy its value to `"pharmacy"` and delete the old key. Add this to `player.go` or to the account loading logic.
+
+### Migration code (in `internal/player/` account loader or `New()`)
+
+After unmarshaling a Player from YAML, add:
+```go
+if xp, ok := p.Skills["alchemy"]; ok {
+ p.Skills[Pharmacy] = xp
+ delete(p.Skills, "alchemy")
+}
+```
+
+Place this in the `AccountStore.LoadCharacter()` function (or wherever character YAML is deserialized) right after the unmarshal call.
+
+---
+
+## 3. Commands
+
+### `clean` -- Free command (background action)
+
+| Property | Value |
+|---|---|
+| Classification | `ClassFree` |
+| Verb | `clean` |
+| Action type | Background (like `fletch`) |
+| ActionType const | `ActionCleaning` |
+| Station required | No |
+| Tick interval | 2 ticks per herb |
+| Cancels active action | No |
+| Cancels background action | Yes (replaces fletching etc.) |
+
+**Usage:**
+- `clean` -- Start cleaning all grimy herbs in inventory (auto-selects, processes one every 2 ticks)
+- `clean <herb>` -- Clean only grimy herbs matching `<herb>` (e.g., `clean guam` cleans only grimy guam)
+
+**Behavior:** The player types `clean`. The command scans inventory for any item that has a `clean` type recipe. It starts a background action that cleans one herb every 2 ticks. After each herb is cleaned, it scans for the next. It stops when no more grimy herbs remain (or no more that match the filter). Outputs a message per herb cleaned. Can be done while walking, fighting, etc. (background action).
+
+### `mix` -- Active command (production)
+
+| Property | Value |
+|---|---|
+| Classification | `ClassActive` |
+| Verb | `mix` |
+| Action type | Active (uses standard production) |
+| ActionType key | `"mix"` in productionTypes |
+| Station required | No |
+| Tick interval | Per recipe `wait` field (default 4) |
+
+**Usage:**
+- `mix` -- Show table of available pharmacy recipes, select by number/name
+- `mix <potion>` -- Mix a specific potion
+- `mix 10 <potion>` -- Mix a specific quantity
+
+**Behavior:** Works like `cook` but with no station requirement. Shows a production table of all `pharmacy` type recipes the player can make. Uses standard production cycle (start message, timed loop, success check, XP, output).
+
+### Command dispatch additions
+
+**`classifyCommand()` in `game.go`:**
+```go
+case "eat", "fletch", "clean":
+ return ClassFree
+```
+And:
+```go
+case "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix":
+ return ClassActive
+```
+
+**`executeCommand()` in `game.go`:**
+Add two new cases:
+```go
+case "clean":
+ g.doClean(sess, strings.Join(args, " "))
+ return
+case "mix":
+ g.doMix(sess, strings.Join(args, " "))
+ return
+```
+
+### Option additions
+
+Add to `OptionDefs` in `player.go`:
+```go
+{"mix_all", OptBool, false, nil, "Auto-start mixing when only one product is possible"},
+```
+
+---
+
+## 4. New Files to Create
+
+### Go files (in `internal/game/`)
+
+| File | Purpose |
+|---|---|
+| `cmd_clean.go` | `doClean()` handler -- find grimy herbs, start background clean action |
+| `cmd_mix.go` | `doMix()` handler -- find pharmacy recipes, show table or start production |
+| `action_clean.go` | `startCleanAction()`, `advanceClean()` -- background action lifecycle |
+
+### YAML data files
+
+| Path | Count | Description |
+|---|---|---|
+| `data/items/grimy_*.yaml` | 14 | Grimy herb items |
+| `data/items/<herb>.yaml` | 14 | Clean herb items (guam, marrentill, etc.) |
+| `data/items/vial_of_water.yaml` | 1 | Vial of water |
+| `data/items/vial.yaml` | 1 | Empty vial |
+| `data/items/*_unf.yaml` | 14 | Unfinished potions |
+| `data/items/<potion>.yaml` | 16 | Finished potions |
+| `data/items/<reagent>.yaml` | ~11 | Secondary ingredients |
+| `data/recipes/clean_*.yaml` | 14 | Clean recipes (type: clean) |
+| `data/recipes/mix_*.yaml` | 16 | Mix recipes (type: pharmacy) |
+| `data/help/clean.yaml` | 1 | Help for clean command |
+| `data/help/mix.yaml` | 1 | Help for mix command |
+| `data/help/pharmacy.yaml` | 1 | Help for pharmacy skill |
+
+**Total: ~102 new YAML files, 3 new Go files.**
+
+---
+
+## 5. Code Changes to Existing Files
+
+### `internal/player/player.go`
+
+1. **Rename constant** (line 24):
+ ```
+ OLD: Alchemy SkillName = "alchemy"
+ NEW: Pharmacy SkillName = "pharmacy"
+ ```
+
+2. **Update AllSkills** (line 37):
+ ```
+ OLD: Alchemy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming,
+ NEW: Pharmacy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming,
+ ```
+
+3. **Update SkillAbbr** (line 56):
+ ```
+ OLD: Alchemy: "alc",
+ NEW: Pharmacy: "pha",
+ ```
+
+4. **Add option** to `OptionDefs` slice (after `smelt_all`):
+ ```go
+ {"mix_all", OptBool, false, nil, "Auto-start mixing when only one product is possible"},
+ ```
+
+5. **Add migration** in the character loading path. In `internal/player/account.go` (or wherever `LoadCharacter` unmarshals), after unmarshal add:
+ ```go
+ if xp, ok := p.Skills["alchemy"]; ok {
+ p.Skills[Pharmacy] = xp
+ delete(p.Skills, "alchemy")
+ }
+ ```
+
+### `internal/game/game.go`
+
+1. **`classifyCommand()`** -- Add `"clean"` to the `ClassFree` case and `"mix"` to the `ClassActive` case:
+ ```go
+ case "eat", "fletch", "clean":
+ return ClassFree
+ ```
+ ```go
+ case "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix":
+ return ClassActive
+ ```
+
+2. **`executeCommand()`** -- Add two cases before the `default:` block:
+ ```go
+ case "clean":
+ g.doClean(sess, strings.Join(args, " "))
+ return
+ case "mix":
+ g.doMix(sess, strings.Join(args, " "))
+ return
+ ```
+
+### `internal/game/action_state.go`
+
+1. **Add ActionType constant:**
+ ```go
+ ActionCleaning ActionType = "cleaning"
+ ActionMixing ActionType = "mixing"
+ ```
+
+2. **Add Description() cases:**
+ ```go
+ case ActionCleaning:
+ return "cleaning herbs"
+ case ActionMixing:
+ return "mixing " + a.TargetName
+ ```
+
+### `internal/game/action.go`
+
+1. **Add `"clean"` to the background action advance switch** (in `AdvanceActions`, around line 254):
+ ```go
+ switch p.BackgroundAction.Type {
+ case "fletch":
+ g.advanceFletch(sess, p)
+ case "clean":
+ g.advanceClean(sess, p)
+ }
+ ```
+
+### `internal/game/action_production.go`
+
+1. **Add pharmacy to `productionTypes` map:**
+ ```go
+ var productionTypes = map[string]productionTypeInfo{
+ "cooking": {"cook", "cooking"},
+ "smelting": {"smelt", "smelting"},
+ "smithing": {"smith", "smithing"},
+ "crafting": {"craft", "crafting"},
+ "combine": {"combine", "combining"},
+ "fletching": {"fletch", "fletching"},
+ "pharmacy": {"mix", "mixing"},
+ }
+ ```
+
+### `internal/object/item.go`
+
+No changes needed. Grimy herbs and clean herbs are normal items. The `clean_into` relationship is encoded in the recipe system, not on ItemDef.
+
+### `internal/net/server.go`
+
+No new session states needed. The `mix` command uses existing `StateProductChoice` and `StateHowMany`. The `clean` command doesn't prompt -- it just starts.
+
+### `AGENTS.md`
+
+Update all references from "Alchemy" to "Pharmacy" and "alc" to "pha". Update the Skills table, the Adding a New Production Skill section, the Commands table (add `clean` and `mix`), and the Action Types table (add `cleaning` and `mixing`).
+
+---
+
+## 6. Herbs -- Full Item YAML
+
+All herbs share a common pattern. Grimy herbs are uncleaned; clean herbs are the output.
+
+### Grimy Herbs
+
+Each grimy herb follows this template:
+
+```yaml
+id: grimy_<name>
+name: "grimy <display_name>"
+color: "65"
+description: "A grimy <display_name> leaf. It needs to be cleaned before it can be used."
+value: <value>
+stackable: false
+```
+
+#### `data/items/grimy_guam.yaml`
+```yaml
+id: grimy_guam
+name: "grimy guam"
+color: "65"
+description: "A grimy guam leaf. It needs to be cleaned before it can be used."
+value: 2
+stackable: false
+```
+
+#### `data/items/grimy_marrentill.yaml`
+```yaml
+id: grimy_marrentill
+name: "grimy marrentill"
+color: "65"
+description: "A grimy marrentill leaf. It needs to be cleaned before it can be used."
+value: 4
+stackable: false
+```
+
+#### `data/items/grimy_tarromin.yaml`
+```yaml
+id: grimy_tarromin
+name: "grimy tarromin"
+color: "65"
+description: "A grimy tarromin leaf. It needs to be cleaned before it can be used."
+value: 7
+stackable: false
+```
+
+#### `data/items/grimy_harralander.yaml`
+```yaml
+id: grimy_harralander
+name: "grimy harralander"
+color: "65"
+description: "A grimy harralander leaf. It needs to be cleaned before it can be used."
+value: 12
+stackable: false
+```
+
+#### `data/items/grimy_ranarr.yaml`
+```yaml
+id: grimy_ranarr
+name: "grimy ranarr"
+color: "65"
+description: "A grimy ranarr leaf. It needs to be cleaned before it can be used."
+value: 25
+stackable: false
+```
+
+#### `data/items/grimy_toadflax.yaml`
+```yaml
+id: grimy_toadflax
+name: "grimy toadflax"
+color: "65"
+description: "A grimy toadflax leaf. It needs to be cleaned before it can be used."
+value: 18
+stackable: false
+```
+
+#### `data/items/grimy_irit.yaml`
+```yaml
+id: grimy_irit
+name: "grimy irit"
+color: "65"
+description: "A grimy irit leaf. It needs to be cleaned before it can be used."
+value: 30
+stackable: false
+```
+
+#### `data/items/grimy_avantoe.yaml`
+```yaml
+id: grimy_avantoe
+name: "grimy avantoe"
+color: "65"
+description: "A grimy avantoe leaf. It needs to be cleaned before it can be used."
+value: 35
+stackable: false
+```
+
+#### `data/items/grimy_kwuarm.yaml`
+```yaml
+id: grimy_kwuarm
+name: "grimy kwuarm"
+color: "65"
+description: "A grimy kwuarm leaf. It needs to be cleaned before it can be used."
+value: 40
+stackable: false
+```
+
+#### `data/items/grimy_snapdragon.yaml`
+```yaml
+id: grimy_snapdragon
+name: "grimy snapdragon"
+color: "65"
+description: "A grimy snapdragon leaf. It needs to be cleaned before it can be used."
+value: 50
+stackable: false
+```
+
+#### `data/items/grimy_cadantine.yaml`
+```yaml
+id: grimy_cadantine
+name: "grimy cadantine"
+color: "65"
+description: "A grimy cadantine leaf. It needs to be cleaned before it can be used."
+value: 55
+stackable: false
+```
+
+#### `data/items/grimy_lantadyme.yaml`
+```yaml
+id: grimy_lantadyme
+name: "grimy lantadyme"
+color: "65"
+description: "A grimy lantadyme leaf. It needs to be cleaned before it can be used."
+value: 60
+stackable: false
+```
+
+#### `data/items/grimy_dwarf_weed.yaml`
+```yaml
+id: grimy_dwarf_weed
+name: "grimy dwarf weed"
+color: "65"
+description: "A grimy dwarf weed leaf. It needs to be cleaned before it can be used."
+value: 65
+stackable: false
+```
+
+#### `data/items/grimy_torstol.yaml`
+```yaml
+id: grimy_torstol
+name: "grimy torstol"
+color: "65"
+description: "A grimy torstol leaf. It needs to be cleaned before it can be used."
+value: 75
+stackable: false
+```
+
+### Clean Herbs
+
+Each clean herb follows this template:
+
+```yaml
+id: <name>
+name: "<display_name>"
+color: "48"
+description: "A clean <display_name> leaf, ready for use in pharmacy."
+value: <value>
+stackable: false
+```
+
+#### `data/items/guam.yaml`
+```yaml
+id: guam
+name: "guam"
+color: "48"
+description: "A clean guam leaf, ready for use in pharmacy."
+value: 3
+stackable: false
+```
+
+#### `data/items/marrentill.yaml`
+```yaml
+id: marrentill
+name: "marrentill"
+color: "48"
+description: "A clean marrentill leaf, ready for use in pharmacy."
+value: 6
+stackable: false
+```
+
+#### `data/items/tarromin.yaml`
+```yaml
+id: tarromin
+name: "tarromin"
+color: "48"
+description: "A clean tarromin leaf, ready for use in pharmacy."
+value: 10
+stackable: false
+```
+
+#### `data/items/harralander.yaml`
+```yaml
+id: harralander
+name: "harralander"
+color: "48"
+description: "A clean harralander leaf, ready for use in pharmacy."
+value: 16
+stackable: false
+```
+
+#### `data/items/ranarr.yaml`
+```yaml
+id: ranarr
+name: "ranarr"
+color: "48"
+description: "A clean ranarr leaf, ready for use in pharmacy."
+value: 35
+stackable: false
+```
+
+#### `data/items/toadflax.yaml`
+```yaml
+id: toadflax
+name: "toadflax"
+color: "48"
+description: "A clean toadflax leaf, ready for use in pharmacy."
+value: 25
+stackable: false
+```
+
+#### `data/items/irit.yaml`
+```yaml
+id: irit
+name: "irit"
+color: "48"
+description: "A clean irit leaf, ready for use in pharmacy."
+value: 40
+stackable: false
+```
+
+#### `data/items/avantoe.yaml`
+```yaml
+id: avantoe
+name: "avantoe"
+color: "48"
+description: "A clean avantoe leaf, ready for use in pharmacy."
+value: 48
+stackable: false
+```
+
+#### `data/items/kwuarm.yaml`
+```yaml
+id: kwuarm
+name: "kwuarm"
+color: "48"
+description: "A clean kwuarm leaf, ready for use in pharmacy."
+value: 54
+stackable: false
+```
+
+#### `data/items/snapdragon.yaml`
+```yaml
+id: snapdragon
+name: "snapdragon"
+color: "48"
+description: "A clean snapdragon leaf, ready for use in pharmacy."
+value: 65
+stackable: false
+```
+
+#### `data/items/cadantine.yaml`
+```yaml
+id: cadantine
+name: "cadantine"
+color: "48"
+description: "A clean cadantine leaf, ready for use in pharmacy."
+value: 70
+stackable: false
+```
+
+#### `data/items/lantadyme.yaml`
+```yaml
+id: lantadyme
+name: "lantadyme"
+color: "48"
+description: "A clean lantadyme leaf, ready for use in pharmacy."
+value: 78
+stackable: false
+```
+
+#### `data/items/dwarf_weed.yaml`
+```yaml
+id: dwarf_weed
+name: "dwarf weed"
+color: "48"
+description: "A clean dwarf weed leaf, ready for use in pharmacy."
+value: 85
+stackable: false
+```
+
+#### `data/items/torstol.yaml`
+```yaml
+id: torstol
+name: "torstol"
+color: "48"
+description: "A clean torstol leaf, ready for use in pharmacy."
+value: 100
+stackable: false
+```
+
+---
+
+## 7. Reagents -- Full Item YAML
+
+Secondary ingredients used to finish potions. Some are sci-fi renamed.
+
+#### `data/items/eye_of_newt.yaml`
+```yaml
+id: eye_of_newt
+name: "eye of newt"
+color: "215"
+description: "A preserved eye of newt, suspended in synthetic fluid. Used in pharmacy."
+value: 3
+stackable: false
+```
+
+#### `data/items/limpwurt_root.yaml`
+```yaml
+id: limpwurt_root
+name: "limpwurt root"
+color: "130"
+description: "A thick, gnarled root with natural stimulant properties."
+value: 8
+stackable: false
+```
+
+#### `data/items/arachnid_enzyme.yaml`
+```yaml
+id: arachnid_enzyme
+name: "arachnid enzyme"
+color: "196"
+description: "A viscous red enzyme harvested from spider egg sacs. Potent catalytic agent."
+value: 12
+stackable: false
+```
+
+#### `data/items/antitoxin_powder.yaml`
+```yaml
+id: antitoxin_powder
+name: "antitoxin powder"
+color: "255"
+description: "A fine white powder derived from synthetic horn material. Neutralizes biological toxins."
+value: 15
+stackable: false
+```
+
+#### `data/items/white_berries.yaml`
+```yaml
+id: white_berries
+name: "white berries"
+color: "255"
+description: "Pale berries from a hardy bush. They have defensive biochemical properties."
+value: 10
+stackable: false
+```
+
+#### `data/items/snape_grass.yaml`
+```yaml
+id: snape_grass
+name: "snape grass"
+color: "120"
+description: "A long blade of snape grass. It crackles with faint static charge."
+value: 10
+stackable: false
+```
+
+#### `data/items/chocolate_dust.yaml`
+```yaml
+id: chocolate_dust
+name: "chocolate dust"
+color: "94"
+description: "Finely ground chocolate. A surprisingly effective pharmaceutical binding agent."
+value: 5
+stackable: false
+```
+
+#### `data/items/blight_spore.yaml`
+```yaml
+id: blight_spore
+name: "blight spore"
+color: "58"
+description: "A desiccated fungal spore from the deadlands. Pulsing with residual bio-energy."
+value: 20
+stackable: false
+```
+
+#### `data/items/potato_cactus.yaml`
+```yaml
+id: potato_cactus
+name: "potato cactus"
+color: "106"
+description: "A bulbous cactus with medicinal properties. Thrives in the arid zones."
+value: 18
+stackable: false
+```
+
+#### `data/items/catalyst_wine.yaml`
+```yaml
+id: catalyst_wine
+name: "catalyst wine"
+color: "124"
+description: "A volatile crimson wine infused with unstable compounds. Handle with care."
+value: 30
+stackable: false
+```
+
+#### `data/items/crushed_nest.yaml`
+```yaml
+id: crushed_nest
+name: "crushed nest"
+color: "94"
+description: "Finely crushed bird's nest material. Contains trace minerals used in advanced pharmacy."
+value: 25
+stackable: false
+```
+
+#### `data/items/vial_of_water.yaml`
+```yaml
+id: vial_of_water
+name: "vial of water"
+color: "39"
+description: "A small glass vial filled with purified water. The base for all potions."
+value: 2
+stackable: false
+```
+
+#### `data/items/vial.yaml`
+```yaml
+id: vial
+name: "vial"
+color: "253"
+description: "An empty glass vial."
+value: 1
+stackable: false
+```
+
+---
+
+## 8. Potions -- Full Item YAML
+
+### Unfinished Potions
+
+Each unfinished potion is created by using a clean herb on a vial of water. They use `made_from` for item-on-item combining via the `use` command. No level requirement. No XP. Instant (ticks: 0 means instant combine in the `use` system).
+
+Template:
+```yaml
+id: <name>_unf
+name: "unfinished <display_name>"
+color: "<color>"
+description: "An unfinished potion with <herb> floating in water. Needs a secondary ingredient."
+value: <value>
+stackable: false
+made_from:
+ - items: [<herb_id>]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/stim_potion_unf.yaml`
+```yaml
+id: stim_potion_unf
+name: "unfinished stim potion"
+color: "48"
+description: "An unfinished potion with guam floating in water."
+value: 5
+stackable: false
+made_from:
+ - items: [guam]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/bio_serum_unf.yaml`
+```yaml
+id: bio_serum_unf
+name: "unfinished bio serum"
+color: "48"
+description: "An unfinished potion with marrentill floating in water."
+value: 8
+stackable: false
+made_from:
+ - items: [marrentill]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/amp_potion_unf.yaml`
+```yaml
+id: amp_potion_unf
+name: "unfinished amp potion"
+color: "48"
+description: "An unfinished potion with tarromin floating in water."
+value: 12
+stackable: false
+made_from:
+ - items: [tarromin]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/nano_restore_unf.yaml`
+```yaml
+id: nano_restore_unf
+name: "unfinished nano restore"
+color: "48"
+description: "An unfinished potion with harralander floating in water."
+value: 18
+stackable: false
+made_from:
+ - items: [harralander]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/stim_cell_unf.yaml`
+```yaml
+id: stim_cell_unf
+name: "unfinished stim cell"
+color: "48"
+description: "An unfinished potion with harralander floating in water."
+value: 18
+stackable: false
+made_from:
+ - items: [harralander]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+**Note:** Nano Restore and Stim Cell share the same herb (harralander). The unfinished potions for harralander-based recipes are the same item. Simplify: use a single `harralander_unf` item instead, and both finished potions use it as input.
+
+**Revised approach for shared herbs:** Use a single unfinished potion per herb (named by herb, not by final potion):
+
+#### `data/items/guam_potion_unf.yaml`
+```yaml
+id: guam_potion_unf
+name: "guam potion (unf)"
+color: "48"
+description: "An unfinished potion with guam floating in water."
+value: 5
+stackable: false
+made_from:
+ - items: [guam]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/marrentill_potion_unf.yaml`
+```yaml
+id: marrentill_potion_unf
+name: "marrentill potion (unf)"
+color: "48"
+description: "An unfinished potion with marrentill floating in water."
+value: 8
+stackable: false
+made_from:
+ - items: [marrentill]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/tarromin_potion_unf.yaml`
+```yaml
+id: tarromin_potion_unf
+name: "tarromin potion (unf)"
+color: "48"
+description: "An unfinished potion with tarromin floating in water."
+value: 12
+stackable: false
+made_from:
+ - items: [tarromin]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/harralander_potion_unf.yaml`
+```yaml
+id: harralander_potion_unf
+name: "harralander potion (unf)"
+color: "48"
+description: "An unfinished potion with harralander floating in water."
+value: 18
+stackable: false
+made_from:
+ - items: [harralander]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/ranarr_potion_unf.yaml`
+```yaml
+id: ranarr_potion_unf
+name: "ranarr potion (unf)"
+color: "48"
+description: "An unfinished potion with ranarr floating in water."
+value: 38
+stackable: false
+made_from:
+ - items: [ranarr]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/toadflax_potion_unf.yaml`
+```yaml
+id: toadflax_potion_unf
+name: "toadflax potion (unf)"
+color: "48"
+description: "An unfinished potion with toadflax floating in water."
+value: 28
+stackable: false
+made_from:
+ - items: [toadflax]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/irit_potion_unf.yaml`
+```yaml
+id: irit_potion_unf
+name: "irit potion (unf)"
+color: "48"
+description: "An unfinished potion with irit floating in water."
+value: 42
+stackable: false
+made_from:
+ - items: [irit]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/avantoe_potion_unf.yaml`
+```yaml
+id: avantoe_potion_unf
+name: "avantoe potion (unf)"
+color: "48"
+description: "An unfinished potion with avantoe floating in water."
+value: 50
+stackable: false
+made_from:
+ - items: [avantoe]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/kwuarm_potion_unf.yaml`
+```yaml
+id: kwuarm_potion_unf
+name: "kwuarm potion (unf)"
+color: "48"
+description: "An unfinished potion with kwuarm floating in water."
+value: 56
+stackable: false
+made_from:
+ - items: [kwuarm]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/snapdragon_potion_unf.yaml`
+```yaml
+id: snapdragon_potion_unf
+name: "snapdragon potion (unf)"
+color: "48"
+description: "An unfinished potion with snapdragon floating in water."
+value: 68
+stackable: false
+made_from:
+ - items: [snapdragon]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/cadantine_potion_unf.yaml`
+```yaml
+id: cadantine_potion_unf
+name: "cadantine potion (unf)"
+color: "48"
+description: "An unfinished potion with cadantine floating in water."
+value: 72
+stackable: false
+made_from:
+ - items: [cadantine]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/lantadyme_potion_unf.yaml`
+```yaml
+id: lantadyme_potion_unf
+name: "lantadyme potion (unf)"
+color: "48"
+description: "An unfinished potion with lantadyme floating in water."
+value: 80
+stackable: false
+made_from:
+ - items: [lantadyme]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/dwarf_weed_potion_unf.yaml`
+```yaml
+id: dwarf_weed_potion_unf
+name: "dwarf weed potion (unf)"
+color: "48"
+description: "An unfinished potion with dwarf weed floating in water."
+value: 88
+stackable: false
+made_from:
+ - items: [dwarf_weed]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+#### `data/items/torstol_potion_unf.yaml`
+```yaml
+id: torstol_potion_unf
+name: "torstol potion (unf)"
+color: "48"
+description: "An unfinished potion with torstol floating in water."
+value: 102
+stackable: false
+made_from:
+ - items: [torstol]
+ qty: 1
+ - items: [vial_of_water]
+ qty: 1
+ticks: 0
+```
+
+### Finished Potions
+
+All potions are non-stackable. Effects are NOT implemented yet -- just items with descriptions.
+
+| Potion (sci-fi name) | RS equivalent | Herb | Reagent | Level | XP |
+|---|---|---|---|---|---|
+| Stim Potion | Attack potion | guam | eye_of_newt | 3 | 25 |
+| Bio Serum | Antipoison | marrentill | antitoxin_powder | 5 | 38 |
+| Amp Potion | Strength potion | tarromin | limpwurt_root | 12 | 50 |
+| Nano Restore | Stat restore | harralander | arachnid_enzyme | 22 | 63 |
+| Stim Cell | Energy potion | harralander | chocolate_dust | 26 | 68 |
+| Shield Potion | Defence potion | ranarr | white_berries | 30 | 75 |
+| Tech Serum | Prayer potion | ranarr | snape_grass | 38 | 88 |
+| Super Stim | Super attack | irit | eye_of_newt | 45 | 100 |
+| Super Bio Serum | Super antipoison | irit | antitoxin_powder | 48 | 106 |
+| Super Stim Cell | Super energy | avantoe | blight_spore | 52 | 118 |
+| Super Amp | Super strength | kwuarm | limpwurt_root | 55 | 125 |
+| Full Restore | Super restore | snapdragon | arachnid_enzyme | 63 | 143 |
+| Super Shield | Super defence | cadantine | white_berries | 66 | 150 |
+| Targeting Serum | Ranging potion | dwarf_weed | catalyst_wine | 72 | 163 |
+| Science Serum | Magic potion | lantadyme | potato_cactus | 76 | 173 |
+| Restoration Compound | Saradomin brew | toadflax | crushed_nest | 81 | 180 |
+
+#### `data/items/stim_potion.yaml`
+```yaml
+id: stim_potion
+name: "stim potion"
+color: "196"
+description: "A bubbling red potion that temporarily boosts attack capability. [Effect not yet implemented]"
+value: 30
+stackable: false
+```
+
+#### `data/items/bio_serum.yaml`
+```yaml
+id: bio_serum
+name: "bio serum"
+color: "48"
+description: "A clear green serum that neutralizes biological toxins. [Effect not yet implemented]"
+value: 45
+stackable: false
+```
+
+#### `data/items/amp_potion.yaml`
+```yaml
+id: amp_potion
+name: "amp potion"
+color: "255"
+description: "A milky white potion that temporarily amplifies physical strength. [Effect not yet implemented]"
+value: 55
+stackable: false
+```
+
+#### `data/items/nano_restore.yaml`
+```yaml
+id: nano_restore
+name: "nano restore"
+color: "208"
+description: "An orange potion containing nanobots that restore diminished attributes. [Effect not yet implemented]"
+value: 70
+stackable: false
+```
+
+#### `data/items/stim_cell.yaml`
+```yaml
+id: stim_cell
+name: "stim cell"
+color: "226"
+description: "A bright yellow energy supplement that restores stamina. [Effect not yet implemented]"
+value: 65
+stackable: false
+```
+
+#### `data/items/shield_potion.yaml`
+```yaml
+id: shield_potion
+name: "shield potion"
+color: "39"
+description: "A luminous blue potion that temporarily reinforces defensive capability. [Effect not yet implemented]"
+value: 85
+stackable: false
+```
+
+#### `data/items/tech_serum.yaml`
+```yaml
+id: tech_serum
+name: "tech serum"
+color: "51"
+description: "A cyan serum that temporarily enhances technology interface capability. [Effect not yet implemented]"
+value: 100
+stackable: false
+```
+
+#### `data/items/super_stim.yaml`
+```yaml
+id: super_stim
+name: "super stim"
+color: "160"
+description: "A potent dark-red stimulant. Dramatically boosts attack capability. [Effect not yet implemented]"
+value: 120
+stackable: false
+```
+
+#### `data/items/super_bio_serum.yaml`
+```yaml
+id: super_bio_serum
+name: "super bio serum"
+color: "34"
+description: "A concentrated antitoxin. Provides extended poison immunity. [Effect not yet implemented]"
+value: 130
+stackable: false
+```
+
+#### `data/items/super_stim_cell.yaml`
+```yaml
+id: super_stim_cell
+name: "super stim cell"
+color: "220"
+description: "An advanced energy compound that fully restores stamina. [Effect not yet implemented]"
+value: 140
+stackable: false
+```
+
+#### `data/items/super_amp.yaml`
+```yaml
+id: super_amp
+name: "super amp"
+color: "231"
+description: "A dangerously concentrated strength amplifier. Handle carefully. [Effect not yet implemented]"
+value: 155
+stackable: false
+```
+
+#### `data/items/full_restore.yaml`
+```yaml
+id: full_restore
+name: "full restore"
+color: "207"
+description: "A shimmering pink potion. Restores all diminished stats simultaneously. [Effect not yet implemented]"
+value: 200
+stackable: false
+```
+
+#### `data/items/super_shield.yaml`
+```yaml
+id: super_shield
+name: "super shield"
+color: "27"
+description: "A deep indigo potion that provides significant defensive enhancement. [Effect not yet implemented]"
+value: 175
+stackable: false
+```
+
+#### `data/items/targeting_serum.yaml`
+```yaml
+id: targeting_serum
+name: "targeting serum"
+color: "70"
+description: "A dark green serum that sharpens ranged targeting systems. [Effect not yet implemented]"
+value: 190
+stackable: false
+```
+
+#### `data/items/science_serum.yaml`
+```yaml
+id: science_serum
+name: "science serum"
+color: "93"
+description: "A violet serum that heightens scientific cognition and casting ability. [Effect not yet implemented]"
+value: 200
+stackable: false
+```
+
+#### `data/items/restoration_compound.yaml`
+```yaml
+id: restoration_compound
+name: "restoration compound"
+color: "214"
+description: "A golden compound that restores hitpoints and boosts defense, but drains other stats. [Effect not yet implemented]"
+value: 250
+stackable: false
+```
+
+---
+
+## 9. Recipes -- Full YAML
+
+### Clean Recipes (type: clean)
+
+These are used by the `clean` background action. Each has `type: clean` and `skill: pharmacy`.
+
+#### `data/recipes/clean_guam.yaml`
+```yaml
+id: clean_guam
+type: clean
+skill: pharmacy
+level: 3
+xp: 3
+wait: 2
+consume:
+ - items: [grimy_guam]
+ qty: 1
+output: guam
+message: "You clean the grimy guam leaf."
+```
+
+#### `data/recipes/clean_marrentill.yaml`
+```yaml
+id: clean_marrentill
+type: clean
+skill: pharmacy
+level: 5
+xp: 4
+wait: 2
+consume:
+ - items: [grimy_marrentill]
+ qty: 1
+output: marrentill
+message: "You clean the grimy marrentill leaf."
+```
+
+#### `data/recipes/clean_tarromin.yaml`
+```yaml
+id: clean_tarromin
+type: clean
+skill: pharmacy
+level: 11
+xp: 5
+wait: 2
+consume:
+ - items: [grimy_tarromin]
+ qty: 1
+output: tarromin
+message: "You clean the grimy tarromin leaf."
+```
+
+#### `data/recipes/clean_harralander.yaml`
+```yaml
+id: clean_harralander
+type: clean
+skill: pharmacy
+level: 20
+xp: 6
+wait: 2
+consume:
+ - items: [grimy_harralander]
+ qty: 1
+output: harralander
+message: "You clean the grimy harralander leaf."
+```
+
+#### `data/recipes/clean_ranarr.yaml`
+```yaml
+id: clean_ranarr
+type: clean
+skill: pharmacy
+level: 25
+xp: 8
+wait: 2
+consume:
+ - items: [grimy_ranarr]
+ qty: 1
+output: ranarr
+message: "You clean the grimy ranarr leaf."
+```
+
+#### `data/recipes/clean_toadflax.yaml`
+```yaml
+id: clean_toadflax
+type: clean
+skill: pharmacy
+level: 30
+xp: 8
+wait: 2
+consume:
+ - items: [grimy_toadflax]
+ qty: 1
+output: toadflax
+message: "You clean the grimy toadflax leaf."
+```
+
+#### `data/recipes/clean_irit.yaml`
+```yaml
+id: clean_irit
+type: clean
+skill: pharmacy
+level: 40
+xp: 9
+wait: 2
+consume:
+ - items: [grimy_irit]
+ qty: 1
+output: irit
+message: "You clean the grimy irit leaf."
+```
+
+#### `data/recipes/clean_avantoe.yaml`
+```yaml
+id: clean_avantoe
+type: clean
+skill: pharmacy
+level: 48
+xp: 10
+wait: 2
+consume:
+ - items: [grimy_avantoe]
+ qty: 1
+output: avantoe
+message: "You clean the grimy avantoe leaf."
+```
+
+#### `data/recipes/clean_kwuarm.yaml`
+```yaml
+id: clean_kwuarm
+type: clean
+skill: pharmacy
+level: 54
+xp: 11
+wait: 2
+consume:
+ - items: [grimy_kwuarm]
+ qty: 1
+output: kwuarm
+message: "You clean the grimy kwuarm leaf."
+```
+
+#### `data/recipes/clean_snapdragon.yaml`
+```yaml
+id: clean_snapdragon
+type: clean
+skill: pharmacy
+level: 59
+xp: 12
+wait: 2
+consume:
+ - items: [grimy_snapdragon]
+ qty: 1
+output: snapdragon
+message: "You clean the grimy snapdragon leaf."
+```
+
+#### `data/recipes/clean_cadantine.yaml`
+```yaml
+id: clean_cadantine
+type: clean
+skill: pharmacy
+level: 65
+xp: 13
+wait: 2
+consume:
+ - items: [grimy_cadantine]
+ qty: 1
+output: cadantine
+message: "You clean the grimy cadantine leaf."
+```
+
+#### `data/recipes/clean_lantadyme.yaml`
+```yaml
+id: clean_lantadyme
+type: clean
+skill: pharmacy
+level: 67
+xp: 13
+wait: 2
+consume:
+ - items: [grimy_lantadyme]
+ qty: 1
+output: lantadyme
+message: "You clean the grimy lantadyme leaf."
+```
+
+#### `data/recipes/clean_dwarf_weed.yaml`
+```yaml
+id: clean_dwarf_weed
+type: clean
+skill: pharmacy
+level: 70
+xp: 14
+wait: 2
+consume:
+ - items: [grimy_dwarf_weed]
+ qty: 1
+output: dwarf_weed
+message: "You clean the grimy dwarf weed leaf."
+```
+
+#### `data/recipes/clean_torstol.yaml`
+```yaml
+id: clean_torstol
+type: clean
+skill: pharmacy
+level: 75
+xp: 15
+wait: 2
+consume:
+ - items: [grimy_torstol]
+ qty: 1
+output: torstol
+message: "You clean the grimy torstol leaf."
+```
+
+### Mix Recipes (type: pharmacy)
+
+These are used by the `mix` command via the standard production system. No station required (station field omitted or empty).
+
+#### `data/recipes/mix_stim_potion.yaml`
+```yaml
+id: mix_stim_potion
+type: pharmacy
+skill: pharmacy
+level: 3
+xp: 25
+wait: 4
+consume:
+ - items: [guam_potion_unf]
+ qty: 1
+ - items: [eye_of_newt]
+ qty: 1
+output: stim_potion
+message: "You mix a stim potion."
+```
+
+#### `data/recipes/mix_bio_serum.yaml`
+```yaml
+id: mix_bio_serum
+type: pharmacy
+skill: pharmacy
+level: 5
+xp: 38
+wait: 4
+consume:
+ - items: [marrentill_potion_unf]
+ qty: 1
+ - items: [antitoxin_powder]
+ qty: 1
+output: bio_serum
+message: "You mix a bio serum."
+```
+
+#### `data/recipes/mix_amp_potion.yaml`
+```yaml
+id: mix_amp_potion
+type: pharmacy
+skill: pharmacy
+level: 12
+xp: 50
+wait: 4
+consume:
+ - items: [tarromin_potion_unf]
+ qty: 1
+ - items: [limpwurt_root]
+ qty: 1
+output: amp_potion
+message: "You mix an amp potion."
+```
+
+#### `data/recipes/mix_nano_restore.yaml`
+```yaml
+id: mix_nano_restore
+type: pharmacy
+skill: pharmacy
+level: 22
+xp: 63
+wait: 4
+consume:
+ - items: [harralander_potion_unf]
+ qty: 1
+ - items: [arachnid_enzyme]
+ qty: 1
+output: nano_restore
+message: "You mix a nano restore."
+```
+
+#### `data/recipes/mix_stim_cell.yaml`
+```yaml
+id: mix_stim_cell
+type: pharmacy
+skill: pharmacy
+level: 26
+xp: 68
+wait: 4
+consume:
+ - items: [harralander_potion_unf]
+ qty: 1
+ - items: [chocolate_dust]
+ qty: 1
+output: stim_cell
+message: "You mix a stim cell."
+```
+
+#### `data/recipes/mix_shield_potion.yaml`
+```yaml
+id: mix_shield_potion
+type: pharmacy
+skill: pharmacy
+level: 30
+xp: 75
+wait: 4
+consume:
+ - items: [ranarr_potion_unf]
+ qty: 1
+ - items: [white_berries]
+ qty: 1
+output: shield_potion
+message: "You mix a shield potion."
+```
+
+#### `data/recipes/mix_tech_serum.yaml`
+```yaml
+id: mix_tech_serum
+type: pharmacy
+skill: pharmacy
+level: 38
+xp: 88
+wait: 4
+consume:
+ - items: [ranarr_potion_unf]
+ qty: 1
+ - items: [snape_grass]
+ qty: 1
+output: tech_serum
+message: "You mix a tech serum."
+```
+
+#### `data/recipes/mix_super_stim.yaml`
+```yaml
+id: mix_super_stim
+type: pharmacy
+skill: pharmacy
+level: 45
+xp: 100
+wait: 4
+consume:
+ - items: [irit_potion_unf]
+ qty: 1
+ - items: [eye_of_newt]
+ qty: 1
+output: super_stim
+message: "You mix a super stim."
+```
+
+#### `data/recipes/mix_super_bio_serum.yaml`
+```yaml
+id: mix_super_bio_serum
+type: pharmacy
+skill: pharmacy
+level: 48
+xp: 106
+wait: 4
+consume:
+ - items: [irit_potion_unf]
+ qty: 1
+ - items: [antitoxin_powder]
+ qty: 1
+output: super_bio_serum
+message: "You mix a super bio serum."
+```
+
+#### `data/recipes/mix_super_stim_cell.yaml`
+```yaml
+id: mix_super_stim_cell
+type: pharmacy
+skill: pharmacy
+level: 52
+xp: 118
+wait: 4
+consume:
+ - items: [avantoe_potion_unf]
+ qty: 1
+ - items: [blight_spore]
+ qty: 1
+output: super_stim_cell
+message: "You mix a super stim cell."
+```
+
+#### `data/recipes/mix_super_amp.yaml`
+```yaml
+id: mix_super_amp
+type: pharmacy
+skill: pharmacy
+level: 55
+xp: 125
+wait: 4
+consume:
+ - items: [kwuarm_potion_unf]
+ qty: 1
+ - items: [limpwurt_root]
+ qty: 1
+output: super_amp
+message: "You mix a super amp."
+```
+
+#### `data/recipes/mix_full_restore.yaml`
+```yaml
+id: mix_full_restore
+type: pharmacy
+skill: pharmacy
+level: 63
+xp: 143
+wait: 4
+consume:
+ - items: [snapdragon_potion_unf]
+ qty: 1
+ - items: [arachnid_enzyme]
+ qty: 1
+output: full_restore
+message: "You mix a full restore."
+```
+
+#### `data/recipes/mix_super_shield.yaml`
+```yaml
+id: mix_super_shield
+type: pharmacy
+skill: pharmacy
+level: 66
+xp: 150
+wait: 4
+consume:
+ - items: [cadantine_potion_unf]
+ qty: 1
+ - items: [white_berries]
+ qty: 1
+output: super_shield
+message: "You mix a super shield."
+```
+
+#### `data/recipes/mix_targeting_serum.yaml`
+```yaml
+id: mix_targeting_serum
+type: pharmacy
+skill: pharmacy
+level: 72
+xp: 163
+wait: 4
+consume:
+ - items: [dwarf_weed_potion_unf]
+ qty: 1
+ - items: [catalyst_wine]
+ qty: 1
+output: targeting_serum
+message: "You mix a targeting serum."
+```
+
+#### `data/recipes/mix_science_serum.yaml`
+```yaml
+id: mix_science_serum
+type: pharmacy
+skill: pharmacy
+level: 76
+xp: 173
+wait: 4
+consume:
+ - items: [lantadyme_potion_unf]
+ qty: 1
+ - items: [potato_cactus]
+ qty: 1
+output: science_serum
+message: "You mix a science serum."
+```
+
+#### `data/recipes/mix_restoration_compound.yaml`
+```yaml
+id: mix_restoration_compound
+type: pharmacy
+skill: pharmacy
+level: 81
+xp: 180
+wait: 4
+consume:
+ - items: [toadflax_potion_unf]
+ qty: 1
+ - items: [crushed_nest]
+ qty: 1
+output: restoration_compound
+message: "You mix a restoration compound."
+```
+
+---
+
+## 10. Clean Mechanic
+
+### Overview
+
+`clean` is a **Free** command that starts a **background action** (like fletching). It auto-scans the player's inventory for grimy herbs and cleans them one at a time.
+
+### Flow
+
+1. Player types `clean` (or `clean <herb_name>`)
+2. `doClean()` in `cmd_clean.go` is called
+3. It loads all recipes with `type: "clean"` from the recipe store
+4. If a filter was provided (e.g., `clean guam`), filter recipes to those whose consume item matches `grimy_<filter>`
+5. Find the first recipe the player has materials AND level for
+6. If none found, output "You don't have any herbs to clean." and return
+7. Cancel any existing background action
+8. Start a background action with type `"clean"` and initial `wait: 1` (phase 0)
+9. Set `BackgroundActionState` to `&ActionState{Type: ActionCleaning}`
+10. Output "You begin cleaning herbs."
+
+### Advance logic (`advanceClean`)
+
+Called every tick when `p.BackgroundAction.Type == "clean"`:
+
+1. **Phase 0** (first tick): Set phase to 1, set WaitLeft to `engine.ToTicks(2)` (2 ticks). Return.
+2. **Phase 1** (production tick):
+ - Load all `type: "clean"` recipes
+ - If a filter was stored in `Data["filter"]`, restrict to matching recipes
+ - Sort recipes by level ascending
+ - Find the first recipe the player has items AND level for
+ - If none found: output "You've run out of herbs to clean.", cancel background action, return
+ - Consume the grimy herb, place the clean herb in the same inventory slot
+ - Award XP via `p.AddSkillXP(player.Pharmacy, recipe.XP)`
+ - Check for level-up, output message
+ - Output the recipe's message (e.g., "You clean the grimy guam leaf.") with XP drop
+ - Save character
+ - Check if there are more herbs to clean (scan again)
+ - If yes: set `WaitLeft = engine.ToTicks(2)`, continue
+ - If no: output "You've finished cleaning herbs.", cancel background action
+
+### In-slot replacement
+
+When cleaning a herb, the clean herb should replace the grimy herb in the **same inventory slot** rather than consuming from one slot and adding to a free slot. This keeps inventory tidy. Implementation:
+
+```go
+for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == recipe.Consume[0].Items[0] {
+ slot.ItemID = recipe.Output
+ break
+ }
+}
+```
+
+This avoids the consume+place pattern and preserves slot position.
+
+### Filter behavior
+
+- `clean` with no args: cleans any grimy herb, cycling through all types
+- `clean guam`: only cleans `grimy_guam`. Stored as `Data["filter"] = "guam"`. Recipe matching checks if the consume item ID contains the filter string.
+
+### Interaction with other actions
+
+- `clean` does NOT cancel the player's active action (gathering, combat, movement)
+- `clean` DOES cancel any existing background action (fletching)
+- Starting a new active action does NOT cancel cleaning (background actions are independent)
+- `get`, `drop`, `quit` cancel the active action but NOT background actions (existing behavior)
+
+---
+
+## 11. Mix Mechanic
+
+### Overview
+
+`mix` is an **Active** command that uses the standard production system. No station required. Works anywhere.
+
+### Flow
+
+1. Player types `mix` (or `mix <potion>` or `mix 10 <potion>`)
+2. `doMix()` in `cmd_mix.go` is called
+3. It loads all recipes with `type: "pharmacy"` from the recipe store
+4. If no args: show production table of all pharmacy recipes (like cook does)
+5. If args provided: match by name, start production if unambiguous
+
+### Implementation (`cmd_mix.go`)
+
+Follow the `cmd_cook.go` pattern closely:
+
+```go
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doMix(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ g.CancelAction(p)
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ if input == "" {
+ g.showMixMenu(sess, p, allRecipes)
+ return
+ }
+
+ qty, itemName := parseQty(input)
+ _ = qty
+
+ // Find matching pharmacy recipes by output name
+ var matched []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "pharmacy" {
+ continue
+ }
+ outDef, _ := g.ItemStore.Load(r.Output)
+ name := r.Output
+ if outDef != nil {
+ name = outDef.Name
+ }
+ if world.WordPrefixMatch(itemName, name) {
+ matched = append(matched, r)
+ }
+ }
+
+ if len(matched) == 0 {
+ sess.WriteLine("You can't mix that.")
+ return
+ }
+
+ // Filter to available
+ var available []action.RecipeDef
+ for _, r := range matched {
+ if r.HasAllItems(p.HasItem) && p.Level(player.Pharmacy) >= r.Level {
+ available = append(available, r)
+ }
+ }
+
+ if len(available) == 0 {
+ sess.WriteLine("You don't have the materials for that.")
+ return
+ }
+
+ if len(available) == 1 {
+ g.promptHowMany(sess, available[0].ID)
+ return
+ }
+
+ // Ambiguous -- show menu
+ sess.State = net.StateRecipeChoice
+ var names []string
+ for _, r := range available {
+ names = append(names, g.recipeName(sess, &r))
+ }
+ g.showMenuTable(sess, "What would you like to mix?", names)
+ sess.PendingMenu = recipeMenuData(available)
+}
+
+func (g *Game) showMixMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef) {
+ seen := make(map[string]bool)
+ var entries []recipeEntry
+
+ for _, r := range allRecipes {
+ if r.Type != "pharmacy" {
+ continue
+ }
+ if seen[r.ID] {
+ continue
+ }
+ if !r.HasAllItems(p.HasItem) {
+ continue
+ }
+ seen[r.ID] = true
+ entries = append(entries, recipeEntry{g.recipeName(sess, &r), r})
+ }
+
+ if len(entries) == 0 {
+ sess.WriteLine("You don't have anything you can mix.")
+ return
+ }
+
+ if len(entries) == 1 {
+ if p.OptionBool("mix_all") {
+ g.startProductionFromRecipe(sess, p, &entries[0].Recipe, 0)
+ return
+ }
+ g.promptHowMany(sess, entries[0].Recipe.ID)
+ return
+ }
+
+ sess.State = net.StateRecipeChoice
+ var names []string
+ for _, e := range entries {
+ names = append(names, e.ItemName)
+ }
+ g.showMenuTable(sess, "What would you like to mix?", names)
+ sess.PendingMenu = entryMenuData(entries)
+}
+```
+
+### `use` command integration
+
+The `use` command already handles MadeFrom combines. When a player does `use guam on vial of water`, it will find the `guam_potion_unf` item's MadeFrom definition and create the combine recipe automatically. No extra code needed.
+
+When a player does `use eye_of_newt on guam potion`, the `use` command should also check pharmacy recipes via `RecipeStore.FindByItems()`. This already works because `FindByItems` searches all recipe types. The production system will handle it.
+
+---
+
+## 12. Action Lifecycle
+
+### Clean Action Lifecycle
+
+**Files:** `cmd_clean.go`, `action_clean.go`
+
+#### `cmd_clean.go` -- `doClean(sess, input)`
+
+1. Get player
+2. Check not in combat (if so, "You can't do that during combat!")
+3. Load all recipes with `type == "clean"`
+4. If `input` provided, filter recipes to those whose consume items match the filter
+5. Find any recipe the player can do (has items + has level)
+6. If none: "You don't have any herbs to clean." return
+7. Cancel existing background action
+8. Set `p.BackgroundAction`:
+ ```go
+ p.BackgroundAction = &action.Action{
+ Type: "clean",
+ TargetID: "clean_herbs",
+ Data: map[string]any{
+ "phase": 0,
+ "filter": input, // "" for all herbs
+ },
+ WaitLeft: engine.ToTicks(1),
+ }
+ p.BackgroundActionState = &ActionState{Type: ActionCleaning}
+ ```
+9. Output: `"\nYou begin cleaning herbs."`
+
+#### `action_clean.go` -- `advanceClean(sess, p)`
+
+```go
+func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
+ phase, _ := p.BackgroundAction.Data["phase"].(int)
+ filter, _ := p.BackgroundAction.Data["filter"].(string)
+
+ if phase == 0 {
+ p.BackgroundAction.Data["phase"] = 1
+ p.BackgroundAction.WaitLeft = engine.ToTicks(2)
+ return
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ // Find first cleanable herb
+ var recipe *action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "clean" {
+ continue
+ }
+ if filter != "" {
+ // Check if this recipe's output or consume matches the filter
+ if !strings.Contains(r.Consume[0].Items[0], filter) &&
+ !strings.Contains(r.Output, filter) {
+ continue
+ }
+ }
+ if p.Level(player.Pharmacy) < r.Level {
+ continue
+ }
+ if !r.HasAllItems(p.HasItem) {
+ continue
+ }
+ recipe = &r
+ break
+ }
+
+ if recipe == nil {
+ sess.WriteLine("\nYou've finished cleaning herbs.")
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ // In-slot replacement
+ consumeID := recipe.Consume[0].Items[0]
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == consumeID {
+ slot.ItemID = recipe.Output
+ break
+ }
+ }
+
+ // Award XP
+ if recipe.XP > 0 {
+ if newLevel := p.AddSkillXP(player.Pharmacy, recipe.XP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up",
+ fmt.Sprintf("*** You are now level %d pharmacy! ***", newLevel)))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ // Output message
+ msg := recipe.Message
+ if msg == "" {
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+ msg = fmt.Sprintf("You clean a %s.", outputName)
+ }
+ if p.OptionBool("xp_drops") && recipe.XP > 0 {
+ abbr := player.SkillAbbr[player.Pharmacy]
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr))
+ }
+ sess.WriteLine(msg)
+
+ // Check for more herbs
+ hasMore := false
+ for _, r := range allRecipes {
+ if r.Type != "clean" {
+ continue
+ }
+ if filter != "" && !strings.Contains(r.Consume[0].Items[0], filter) &&
+ !strings.Contains(r.Output, filter) {
+ continue
+ }
+ if p.Level(player.Pharmacy) >= r.Level && r.HasAllItems(p.HasItem) {
+ hasMore = true
+ break
+ }
+ }
+
+ if !hasMore {
+ sess.WriteLine("\nYou've finished cleaning herbs.")
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ p.BackgroundAction.WaitLeft = engine.ToTicks(2)
+}
+```
+
+### Mix Action Lifecycle
+
+Mix uses the standard production system entirely. No custom advance function needed. The `advanceProduction()` function handles it because `"mix"` is registered in `productionActionTypes` via `productionTypes`.
+
+Flow:
+1. `doMix()` -> user selects recipe -> `promptHowMany()` -> `handleHowMany()` -> `startProductionFromRecipe()`
+2. `startProductionFromRecipe()` creates `p.Action` with type `"mix"`, actionType from `productionTypes["pharmacy"]`
+3. Each tick, `AdvanceActions()` calls `advanceProduction()` for `"mix"` type
+4. Production cycle: start message -> wait -> skill check -> consume + output -> XP -> repeat or end
+
+---
+
+## 13. XP Table
+
+### Herb Cleaning XP
+
+| Herb | Pharmacy Level | Clean XP |
+|---|---|---|
+| Guam | 3 | 3 |
+| Marrentill | 5 | 4 |
+| Tarromin | 11 | 5 |
+| Harralander | 20 | 6 |
+| Ranarr | 25 | 8 |
+| Toadflax | 30 | 8 |
+| Irit | 40 | 9 |
+| Avantoe | 48 | 10 |
+| Kwuarm | 54 | 11 |
+| Snapdragon | 59 | 12 |
+| Cadantine | 65 | 13 |
+| Lantadyme | 67 | 13 |
+| Dwarf Weed | 70 | 14 |
+| Torstol | 75 | 15 |
+
+### Potion Mixing XP
+
+| Potion | Pharmacy Level | Mix XP | Herb | Reagent |
+|---|---|---|---|---|
+| Stim Potion | 3 | 25 | Guam | Eye of Newt |
+| Bio Serum | 5 | 38 | Marrentill | Antitoxin Powder |
+| Amp Potion | 12 | 50 | Tarromin | Limpwurt Root |
+| Nano Restore | 22 | 63 | Harralander | Arachnid Enzyme |
+| Stim Cell | 26 | 68 | Harralander | Chocolate Dust |
+| Shield Potion | 30 | 75 | Ranarr | White Berries |
+| Tech Serum | 38 | 88 | Ranarr | Snape Grass |
+| Super Stim | 45 | 100 | Irit | Eye of Newt |
+| Super Bio Serum | 48 | 106 | Irit | Antitoxin Powder |
+| Super Stim Cell | 52 | 118 | Avantoe | Blight Spore |
+| Super Amp | 55 | 125 | Kwuarm | Limpwurt Root |
+| Full Restore | 63 | 143 | Snapdragon | Arachnid Enzyme |
+| Super Shield | 66 | 150 | Cadantine | White Berries |
+| Targeting Serum | 72 | 163 | Dwarf Weed | Catalyst Wine |
+| Science Serum | 76 | 173 | Lantadyme | Potato Cactus |
+| Restoration Compound | 81 | 180 | Toadflax | Crushed Nest |
+
+### Unfinished Potions
+
+Adding a clean herb to a vial of water gives **0 XP** and has **no level requirement**. This is an instant combine via MadeFrom, not a recipe.
+
+### Total XP per potion (clean + mix)
+
+Example: Stim Potion = 3 (clean guam) + 25 (mix) = 28 total pharmacy XP per potion.
+
+---
+
+## 14. Mob Drops
+
+Grimy herbs should be added to existing mob drop tables. Higher-level mobs drop rarer herbs.
+
+### Existing mobs to modify
+
+#### `data/mobs/man.yaml` -- Add to loot table:
+```yaml
+ - item_id: "grimy_guam"
+ weight: 8
+ - item_id: "grimy_marrentill"
+ weight: 4
+ - item_id: "grimy_tarromin"
+ weight: 2
+```
+
+#### `data/mobs/guard.yaml` -- Add to loot table:
+```yaml
+ - item_id: "grimy_guam"
+ weight: 6
+ - item_id: "grimy_marrentill"
+ weight: 5
+ - item_id: "grimy_tarromin"
+ weight: 4
+ - item_id: "grimy_harralander"
+ weight: 3
+ - item_id: "grimy_ranarr"
+ weight: 1
+```
+
+### Suggested new mobs (future)
+
+Higher-tier mobs (when added) should drop higher-tier herbs. Guidelines:
+
+| Mob Combat Level | Herb Drops |
+|---|---|
+| 1-10 | Guam, Marrentill |
+| 10-25 | Tarromin, Harralander |
+| 25-50 | Ranarr, Toadflax, Irit |
+| 50-75 | Avantoe, Kwuarm, Snapdragon |
+| 75-100 | Cadantine, Lantadyme |
+| 100+ | Dwarf Weed, Torstol |
+
+### Shared drop table (optional)
+
+Create `data/drops/herb_table_low.yaml` etc. for reusable herb drop tables:
+
+#### `data/drops/herb_table_low.yaml`
+```yaml
+id: herb_table_low
+entries:
+ - item_id: grimy_guam
+ weight: 10
+ - item_id: grimy_marrentill
+ weight: 7
+ - item_id: grimy_tarromin
+ weight: 4
+ - item_id: grimy_harralander
+ weight: 2
+```
+
+#### `data/drops/herb_table_mid.yaml`
+```yaml
+id: herb_table_mid
+entries:
+ - item_id: grimy_harralander
+ weight: 8
+ - item_id: grimy_ranarr
+ weight: 6
+ - item_id: grimy_toadflax
+ weight: 5
+ - item_id: grimy_irit
+ weight: 4
+ - item_id: grimy_avantoe
+ weight: 2
+```
+
+#### `data/drops/herb_table_high.yaml`
+```yaml
+id: herb_table_high
+entries:
+ - item_id: grimy_kwuarm
+ weight: 7
+ - item_id: grimy_snapdragon
+ weight: 5
+ - item_id: grimy_cadantine
+ weight: 4
+ - item_id: grimy_lantadyme
+ weight: 3
+ - item_id: grimy_dwarf_weed
+ weight: 2
+ - item_id: grimy_torstol
+ weight: 1
+```
+
+Mobs can reference these via `table: herb_table_low` in their loot entries (existing drop table system).
+
+### Reagent sources
+
+Reagents are obtained from:
+- **Shops** (future shop system): Eye of Newt, Vial of Water, Vial, Chocolate Dust
+- **Mob drops**: Limpwurt Root, Arachnid Enzyme, White Berries, Snape Grass, Antitoxin Powder
+- **Gathering** (future): Blight Spore, Potato Cactus
+- **Other skills**: Crushed Nest (from search on bird's nest), Catalyst Wine (future quest/gathering)
+
+For initial implementation, consider adding ground spawns or shop items for basic reagents (eye_of_newt, vial_of_water) so players can train pharmacy from level 1.
+
+---
+
+## 15. Help Files
+
+#### `data/help/clean.yaml`
+```yaml
+name: "clean"
+category: "Commands"
+description: |
+ Clean grimy herbs from your inventory.
+
+ Usage: clean Clean all grimy herbs
+ clean <herb> Clean only a specific herb type
+
+ Cleaning is a background action that runs alongside other
+ activities. You can clean herbs while walking, gathering, or
+ even during combat.
+
+ Each herb type requires a minimum Pharmacy level to clean.
+ You earn Pharmacy XP for each herb cleaned.
+
+ The command processes one herb every 2 ticks and automatically
+ moves on to the next grimy herb in your inventory. It stops
+ when you have no more grimy herbs (or no more matching the
+ filter).
+
+ Grimy herbs are obtained as drops from mobs or through farming.
+ Clean herbs are used to create unfinished potions by combining
+ them with a vial of water.
+
+ See also: mix, pharmacy, use
+```
+
+#### `data/help/mix.yaml`
+```yaml
+name: "mix"
+category: "Commands"
+description: |
+ Mix potions from unfinished potions and secondary reagents.
+
+ Usage: mix Show all potions you can mix right now
+ mix <potion> Mix a specific potion
+ mix 10 <potion> Mix a specific quantity
+
+ Mixing does not require a station and can be done anywhere.
+
+ Before mixing starts, you are prompted "How many?" -- press
+ return for all, type a number, or type anything else to cancel.
+
+ The Pharmacy skill check determines success or failure. Higher
+ levels yield higher success rates.
+
+ You can also mix by using items directly:
+ use eye of newt on guam potion
+
+ To create unfinished potions, use a clean herb on a vial
+ of water:
+ use guam on vial of water
+
+ See also: clean, pharmacy, use
+```
+
+#### `data/help/pharmacy.yaml`
+```yaml
+name: "pharmacy"
+category: "Skills"
+description: |
+ Pharmacy is the skill of creating potions from herbs and
+ reagents. It replaces the old Alchemy discipline.
+
+ The production chain:
+ 1. Obtain grimy herbs (mob drops, farming)
+ 2. Clean herbs with the "clean" command (Pharmacy XP)
+ 3. Use clean herbs on vials of water to create unfinished
+ potions (no XP)
+ 4. Mix unfinished potions with reagents using the "mix"
+ command (Pharmacy XP)
+
+ Potions provide temporary stat boosts, healing, and other
+ effects when consumed. Higher-level potions require rarer
+ herbs and reagents.
+
+ Herbs (ordered by level):
+ Guam (3), Marrentill (5), Tarromin (11), Harralander (20),
+ Ranarr (25), Toadflax (30), Irit (40), Avantoe (48),
+ Kwuarm (54), Snapdragon (59), Cadantine (65),
+ Lantadyme (67), Dwarf Weed (70), Torstol (75)
+
+ See also: clean, mix, score
+```
+
+---
+
+## Implementation Order
+
+Recommended sequence for implementation:
+
+1. **Skill rename** -- Change `Alchemy` to `Pharmacy` in `player.go` (3 lines). Add migration code. Update AGENTS.md. Run `make vet` and `make test`.
+
+2. **Add ActionType constants** -- Add `ActionCleaning` and `ActionMixing` to `action_state.go` with `Description()` cases.
+
+3. **Add productionTypes entry** -- Add `"pharmacy"` to `productionTypes` map in `action_production.go`.
+
+4. **Add option** -- Add `mix_all` to `OptionDefs` in `player.go`.
+
+5. **Create all item YAML files** -- All herbs (grimy + clean), reagents, vials, unfinished potions, finished potions. Total ~57 items.
+
+6. **Create all recipe YAML files** -- 14 clean recipes + 16 mix recipes. Total 30 recipes.
+
+7. **Create `cmd_mix.go`** -- Follow `cmd_cook.go` pattern. Register in `classifyCommand()` and `executeCommand()`.
+
+8. **Create `cmd_clean.go`** -- The doClean handler. Register in `classifyCommand()` and `executeCommand()`.
+
+9. **Create `action_clean.go`** -- The `advanceClean()` background action. Register in `AdvanceActions()` switch.
+
+10. **Update mob drop tables** -- Add grimy herbs to existing mobs. Create shared herb drop tables.
+
+11. **Create help YAML files** -- `clean.yaml`, `mix.yaml`, `pharmacy.yaml`.
+
+12. **Test** -- Run `make build`, `make vet`, `make test`. Connect via telnet and verify:
+ - `score` shows Pharmacy (not Alchemy)
+ - Grimy herbs can be obtained (add to inventory manually or kill mobs)
+ - `clean` works as background action
+ - `use guam on vial of water` creates unfinished potion
+ - `mix` shows available recipes and produces potions
+ - XP is awarded correctly
+ - Level-up messages display correctly
+
+---
+
+## File Checklist
+
+### New Go files (3)
+- [ ] `internal/game/cmd_clean.go`
+- [ ] `internal/game/cmd_mix.go`
+- [ ] `internal/game/action_clean.go`
+
+### Modified Go files (5)
+- [ ] `internal/player/player.go` (rename Alchemy->Pharmacy, add option)
+- [ ] `internal/game/game.go` (classifyCommand, executeCommand)
+- [ ] `internal/game/action_state.go` (ActionCleaning, ActionMixing)
+- [ ] `internal/game/action.go` (AdvanceActions background switch)
+- [ ] `internal/game/action_production.go` (productionTypes map)
+
+### New YAML item files (57)
+- [ ] 14 grimy herbs: `grimy_guam.yaml` through `grimy_torstol.yaml`
+- [ ] 14 clean herbs: `guam.yaml` through `torstol.yaml`
+- [ ] 14 unfinished potions: `guam_potion_unf.yaml` through `torstol_potion_unf.yaml`
+- [ ] 16 finished potions (see Section 8)
+- [ ] 11 reagents (see Section 7)
+- [ ] 2 vials: `vial.yaml`, `vial_of_water.yaml`
+
+### New YAML recipe files (30)
+- [ ] 14 clean recipes: `clean_guam.yaml` through `clean_torstol.yaml`
+- [ ] 16 mix recipes: `mix_stim_potion.yaml` through `mix_restoration_compound.yaml`
+
+### New YAML help files (3)
+- [ ] `data/help/clean.yaml`
+- [ ] `data/help/mix.yaml`
+- [ ] `data/help/pharmacy.yaml`
+
+### New YAML drop tables (3, optional)
+- [ ] `data/drops/herb_table_low.yaml`
+- [ ] `data/drops/herb_table_mid.yaml`
+- [ ] `data/drops/herb_table_high.yaml`
+
+### Modified YAML files (2+)
+- [ ] `data/mobs/man.yaml` (add herb drops)
+- [ ] `data/mobs/guard.yaml` (add herb drops)
+
+### Documentation
+- [ ] `AGENTS.md` (update skill name, add commands, add action types)