aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/pharmacy.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 18:33:43 -0400
committerhistoria <[not public]>2026-06-19 18:33:43 -0400
commit2bec24859af8f03c80a0a58e3240519942450167 (patch)
treedcb8baa5591fdeca51e179a63a08d46b014cb85c /skill_plans/pharmacy.md
parent0ea4eec5dd2122c6704216971eb1249276297867 (diff)
downloadthehouseoficarus-2bec24859af8f03c80a0a58e3240519942450167.tar.gz
readme
Diffstat (limited to 'skill_plans/pharmacy.md')
-rw-r--r--skill_plans/pharmacy.md2421
1 files changed, 0 insertions, 2421 deletions
diff --git a/skill_plans/pharmacy.md b/skill_plans/pharmacy.md
deleted file mode 100644
index 5e0b7af..0000000
--- a/skill_plans/pharmacy.md
+++ /dev/null
@@ -1,2421 +0,0 @@
-# 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)