From 458916fb78dcef3ff77cb29b7ba98d9f646819fc Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 19 Jun 2026 02:08:55 -0400 Subject: feat: rough pharmacy skill added --- skill_plans/pharmacy.md | 2421 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2421 insertions(+) create mode 100644 skill_plans/pharmacy.md (limited to 'skill_plans/pharmacy.md') 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: ` 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 ` -- Clean only grimy herbs matching `` (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 ` -- Mix a specific potion +- `mix 10 ` -- 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/.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/.yaml` | 16 | Finished potions | +| `data/items/.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: "grimy " +color: "65" +description: "A grimy leaf. It needs to be cleaned before it can be used." +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: "" +color: "48" +description: "A clean leaf, ready for use in pharmacy." +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: _unf +name: "unfinished " +color: "" +description: "An unfinished potion with floating in water. Needs a secondary ingredient." +value: +stackable: false +made_from: + - items: [] + 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 `) +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_` +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 ` or `mix 10 `) +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 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 Mix a specific potion + mix 10 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) -- cgit v1.2.3