aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/farming.md
diff options
context:
space:
mode:
Diffstat (limited to 'skill_plans/farming.md')
-rw-r--r--skill_plans/farming.md2123
1 files changed, 2123 insertions, 0 deletions
diff --git a/skill_plans/farming.md b/skill_plans/farming.md
new file mode 100644
index 0000000..689f9ec
--- /dev/null
+++ b/skill_plans/farming.md
@@ -0,0 +1,2123 @@
+# Farming Skill Implementation Plan
+
+## 1. Overview
+
+Farming allows players to plant seeds in farming patches, water them, wait for them to grow through multiple stages, and harvest the results. It is a long-cycle skill: seeds take minutes to grow, with periodic growth ticks advancing them through stages. Disease can strike at each growth stage; watering eliminates that risk. Dead plants must be cleared with a rake before replanting.
+
+The system is simpler than RuneScape: only three tools (rake, spade, watering can), no compost system, and growth only ticks for online players.
+
+**Patch types:** herb patches, allotment patches, flower patches, bush patches, tree patches.
+
+**Core loop:** rake weeds -> plant seed (requires spade) -> water (optional but prevents disease) -> wait for growth -> harvest (requires spade) -> repeat.
+
+**Sci-fi flavor:** Seeds are "bio-engineered seeds," patches are "hydroponic plots," the tool shed is a "supply locker," watering can is a "hydration unit." But mechanically they work identically to RS farming.
+
+---
+
+## 2. Architecture
+
+### Per-Player Farming State via Player Flags
+
+Each farming patch is a world object (e.g., `herb_patch`) placed in a room. However, each player sees their **own** state for that patch. This is achieved using **player flags** (`p.Flags`), not world state.
+
+**Flag naming convention:**
+
+```
+farm_{patch_type}_{patch_index}_{field}
+```
+
+**Fields per patch:**
+
+| Flag Key | Type | Description |
+|---|---|---|
+| `farm_herb_1_seed` | `string` | Seed ID planted (e.g., `"guam_seed"`), empty if unplanted |
+| `farm_herb_1_stage` | `int` | Current growth stage (0 = just planted, N = fully grown) |
+| `farm_herb_1_watered` | `bool` | Whether current stage has been watered |
+| `farm_herb_1_diseased` | `bool` | Whether plant is currently diseased |
+| `farm_herb_1_dead` | `bool` | Whether plant has died (must rake) |
+| `farm_herb_1_weeds` | `bool` | Whether patch has weeds (must rake before planting) |
+| `farm_herb_1_ready` | `bool` | Whether crop is fully grown and ready to harvest |
+
+**Patch index mapping:** Each physical patch object in a room corresponds to a unique patch index. The index is determined by the object's position in the room's `objects` list combined with a patch type prefix. For example, room 150 might have:
+
+```yaml
+objects:
+ - id: herb_patch # farm_herb_1
+ - id: herb_patch # farm_herb_2
+ - id: allotment_patch # farm_allot_1
+```
+
+The mapping from object instance to flag prefix is derived at runtime:
+- Object def ID `herb_patch` with index 0 in room -> flag prefix `farm_herb_1`
+- Object def ID `herb_patch` with index 1 in room -> flag prefix `farm_herb_2`
+- Object def ID `allotment_patch` with index 0 in room -> flag prefix `farm_allot_1`
+
+**Helper function** `farmFlagPrefix(defID string, index int) string`:
+```go
+func farmFlagPrefix(defID string, index int) string {
+ switch defID {
+ case "herb_patch":
+ return fmt.Sprintf("farm_herb_%d", index+1)
+ case "allotment_patch":
+ return fmt.Sprintf("farm_allot_%d", index+1)
+ case "flower_patch":
+ return fmt.Sprintf("farm_flower_%d", index+1)
+ case "bush_patch":
+ return fmt.Sprintf("farm_bush_%d", index+1)
+ case "tree_patch":
+ return fmt.Sprintf("farm_tree_%d", index+1)
+ }
+ return ""
+}
+```
+
+**Why player flags?**
+- World flags are shared by all players. Farming patches must be per-player.
+- Player flags are already persisted to character YAML automatically via `AccountStore.SaveCharacter()`.
+- No new data structures or serialization code needed.
+- The `p.Flags` map is `map[string]any` and supports string, int, bool, and float values natively via YAML serialization.
+
+### Patch State Initialization
+
+When a player first interacts with a farming patch (via `inspect`, `plant`, `rake`, etc.), if no flags exist for that patch, initialize it with weeds:
+
+```go
+func (g *Game) ensureFarmState(p *player.Player, prefix string) {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ if _, exists := p.Flags[prefix+"_seed"]; !exists {
+ p.Flags[prefix+"_weeds"] = true
+ p.Flags[prefix+"_seed"] = ""
+ p.Flags[prefix+"_stage"] = 0
+ p.Flags[prefix+"_watered"] = false
+ p.Flags[prefix+"_diseased"] = false
+ p.Flags[prefix+"_dead"] = false
+ p.Flags[prefix+"_ready"] = false
+ }
+}
+```
+
+### Seed Data Lookup
+
+Each seed item has farming-specific fields. Since `ItemDef` in `internal/object/item.go` is the canonical item definition, we add new fields to `ItemDef`:
+
+```go
+// New fields added to ItemDef struct in internal/object/item.go
+FarmPatchType string `yaml:"farm_patch_type"` // "herb", "allotment", "flower", "bush", "tree"
+FarmLevel int `yaml:"farm_level"` // Required farming level to plant
+FarmPlantXP int `yaml:"farm_plant_xp"` // XP for planting
+FarmHarvestXP int `yaml:"farm_harvest_xp"` // XP per harvest action
+FarmStages int `yaml:"farm_stages"` // Number of growth stages
+FarmProduct string `yaml:"farm_product"` // Item ID produced on harvest
+FarmMinYield int `yaml:"farm_min_yield"` // Minimum harvest quantity
+FarmMaxYield int `yaml:"farm_max_yield"` // Maximum harvest quantity
+```
+
+This keeps the data-driven pattern: seed behavior is defined in YAML, not hardcoded.
+
+---
+
+## 3. Growth Tick
+
+### `FarmTick()` in `internal/game/tick.go`
+
+A new tick function that runs on a **counter-based schedule** rather than every tick. Growth is checked every 500 ticks (approximately 5 minutes at 600ms tick rate).
+
+**Implementation:**
+
+Add a counter field to `Game`:
+
+```go
+// In Game struct (game.go)
+farmTickCounter int
+```
+
+Add `FarmTick()` to `tick.go`:
+
+```go
+const FarmTickInterval = 500 // ticks between farm growth checks (~5 minutes)
+
+func (g *Game) FarmTick() {
+ g.farmTickCounter++
+ if g.farmTickCounter < FarmTickInterval {
+ return
+ }
+ g.farmTickCounter = 0
+
+ if g.Hub == nil {
+ return
+ }
+
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil || p.Flags == nil {
+ continue
+ }
+ g.advanceFarmGrowth(sess, p)
+ }
+}
+```
+
+### `advanceFarmGrowth()` in `internal/game/action_farm.go`
+
+```go
+func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) {
+ // Scan all farming flag prefixes in player flags
+ prefixes := g.findActiveFarmPrefixes(p)
+ for _, prefix := range prefixes {
+ seedID, _ := p.Flags[prefix+"_seed"].(string)
+ if seedID == "" {
+ continue
+ }
+ dead, _ := p.Flags[prefix+"_dead"].(bool)
+ if dead {
+ continue
+ }
+ diseased, _ := p.Flags[prefix+"_diseased"].(bool)
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if ready {
+ continue
+ }
+
+ // If diseased and not cured, plant dies
+ if diseased {
+ p.Flags[prefix+"_dead"] = true
+ p.Flags[prefix+"_diseased"] = false
+ sess.WriteLine(g.colorize(sess, "farm_disease",
+ fmt.Sprintf("\nYour %s has died from disease!", seedDisplayName(g, seedID))))
+ g.AccountStore.SaveCharacter(p)
+ continue
+ }
+
+ // Advance growth stage
+ stage, _ := p.Flags[prefix+"_stage"].(int)
+ watered, _ := p.Flags[prefix+"_watered"].(bool)
+
+ seedDef, err := g.ItemStore.Load(seedID)
+ if err != nil {
+ continue
+ }
+
+ maxStages := seedDef.FarmStages
+ if maxStages <= 0 {
+ maxStages = 4
+ }
+
+ stage++
+
+ if stage >= maxStages {
+ // Fully grown!
+ p.Flags[prefix+"_stage"] = stage
+ p.Flags[prefix+"_ready"] = true
+ p.Flags[prefix+"_watered"] = false
+ sess.WriteLine(g.colorize(sess, "farm_grow",
+ fmt.Sprintf("\nYour %s is fully grown and ready to harvest!",
+ seedDisplayName(g, seedID))))
+ } else {
+ // Disease check (10% chance if not watered)
+ if !watered && rand.Float64() < 0.10 {
+ p.Flags[prefix+"_stage"] = stage
+ p.Flags[prefix+"_diseased"] = true
+ p.Flags[prefix+"_watered"] = false
+ sess.WriteLine(g.colorize(sess, "farm_disease",
+ fmt.Sprintf("\nYour %s has become diseased!",
+ seedDisplayName(g, seedID))))
+ } else {
+ p.Flags[prefix+"_stage"] = stage
+ p.Flags[prefix+"_watered"] = false // Reset watered for next stage
+ sess.WriteLine(g.colorize(sess, "farm_grow",
+ fmt.Sprintf("\nYour %s has grown to stage %d/%d.",
+ seedDisplayName(g, seedID), stage, maxStages)))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+ }
+}
+```
+
+### `findActiveFarmPrefixes()`
+
+Scans `p.Flags` to find all unique farm prefixes that have a planted seed:
+
+```go
+func (g *Game) findActiveFarmPrefixes(p *player.Player) []string {
+ seen := make(map[string]bool)
+ var prefixes []string
+ for key := range p.Flags {
+ if !strings.HasPrefix(key, "farm_") {
+ continue
+ }
+ if !strings.HasSuffix(key, "_seed") {
+ continue
+ }
+ prefix := strings.TrimSuffix(key, "_seed")
+ if !seen[prefix] {
+ seen[prefix] = true
+ if seedID, ok := p.Flags[key].(string); ok && seedID != "" {
+ prefixes = append(prefixes, prefix)
+ }
+ }
+ }
+ sort.Strings(prefixes)
+ return prefixes
+}
+```
+
+### Subscribe in main.go
+
+Add `g.FarmTick()` to the tick subscription in `cmd/mud/main.go`:
+
+```go
+g.Ticks.Subscribe(1, func() bool {
+ g.MoveTick()
+ g.ProcessQueuedCommands()
+ g.World.Tick()
+ g.MobStore.Tick()
+ g.RegenTick()
+ g.DisconnectTick()
+ g.WanderTick()
+ g.SharedDepletionTick()
+ g.FireTick()
+ g.AdvanceActions()
+ g.ConsumeTick()
+ g.BroadcastRespawns()
+ g.VisualTick()
+ g.FarmTick() // <-- ADD THIS
+ return true
+})
+```
+
+### Design Decision: Online-Only Growth
+
+Growth only advances for online players. When a player logs off, their crops freeze in place. This is intentional:
+- Keeps implementation simple (no background timers)
+- Players don't return to find everything dead
+- Matches the "live state" philosophy of the codebase
+
+---
+
+## 4. Commands
+
+### Command Summary
+
+| Command | Class | Description |
+|---|---|---|
+| `plant <seed>` | Active | Plant a seed in the appropriate patch in the current room |
+| `harvest [patch]` | Active | Harvest a fully grown crop from a patch |
+| `rake [patch]` | Active | Clear weeds or dead plants from a patch |
+| `water [patch]` | Active | Water a patch with a watering can |
+| `cure [patch]` | Active | Use plant cure on a diseased patch |
+| `inspect [patch]` | Instant | Check the status of farming patches in the room |
+
+### 4.1 `plant <seed>` (Active)
+
+**Classification:** Add `"plant"` to the Active case in `classifyCommand()`.
+
+**Dispatch:** Add case in `executeCommand()`:
+```go
+case "plant":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ sess.WriteLine("Plant what?")
+ } else {
+ g.doPlant(sess, strings.Join(args, " "))
+ }
+ return
+```
+
+**Handler: `doPlant()`** in `cmd_farm.go`:
+
+1. Find the seed item in player inventory by name match (`findInventoryMatches`).
+2. Load the seed's `ItemDef`. Check `FarmPatchType` is set — if not, "You can't plant that."
+3. Check farming level: `p.Level(player.Farming) >= seedDef.FarmLevel` — if not, "You need level N farming to plant that."
+4. Check the player has a spade: scan inventory and equipment for `tool_type: "spade"`. If not found, "You need a spade to plant seeds."
+5. Find a matching patch object in the room: scan `g.World.FindObjInstances(p.RoomID, seedDef.FarmPatchType+"_patch")`.
+6. If no matching patch in room, "There's no suitable patch here to plant that."
+7. If multiple patches, find the first one that is clear (no weeds, not planted, not dead) using the player's flags for each patch.
+8. If no clear patch, "All patches here have something in them. Rake them first." or "All patches are occupied."
+9. Determine the flag prefix via `farmFlagPrefix(patchDefID, patchIndex)`.
+10. Ensure farm state is initialized.
+11. Check weeds: if `prefix_weeds == true`, "You need to rake the weeds first."
+12. Check already planted: if `prefix_seed != ""`, "Something is already planted here."
+13. Remove 1 seed from inventory.
+14. Set flags: `prefix_seed = seedID`, `prefix_stage = 0`, `prefix_watered = false`, `prefix_diseased = false`, `prefix_dead = false`, `prefix_ready = false`.
+15. Award planting XP: `p.AddSkillXP(player.Farming, seedDef.FarmPlantXP)`.
+16. Save character.
+17. Output: "You plant a guam seed in the herb patch."
+18. Set ActionState: `&ActionState{Type: ActionPlanting, TargetName: seedDef.Name}`.
+19. Set Action with WaitLeft of 3 ticks (planting takes a moment).
+
+**Alternative simpler approach:** Since planting is conceptually instant (just set flags and remove seed), it can be implemented as a direct command handler without a multi-tick action. This matches how `burn` phase 0 works. However, to match the spec request for Active classification, use a 2-tick action:
+
+```go
+p.Action = &action.Action{
+ Type: "plant",
+ TargetID: seedID,
+ TargetName: seedDef.Name,
+ WaitLeft: 2,
+ Data: map[string]any{
+ "seed_id": seedID,
+ "prefix": prefix,
+ "xp": seedDef.FarmPlantXP,
+ },
+}
+p.ActionState = &ActionState{Type: ActionPlanting, TargetName: seedDef.Name}
+```
+
+Then in `advancePlant()`, do the actual flag-setting and item removal.
+
+### 4.2 `harvest [patch]` (Active)
+
+**Classification:** Add `"harvest"` to Active case in `classifyCommand()`.
+
+**Dispatch:** Add case in `executeCommand()`:
+```go
+case "harvest":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doHarvest(sess, "")
+ } else {
+ g.doHarvest(sess, strings.Join(args, " "))
+ }
+ return
+```
+
+**Handler: `doHarvest()`** in `cmd_farm.go`:
+
+1. Find farming patches in the current room.
+2. If `input != ""`, match against patch names (e.g., "herb", "allotment"). Support numbered targeting: `1.herb`.
+3. If `input == ""`, find the first patch that is ready to harvest (smart default).
+4. For the chosen patch, get flag prefix and check `prefix_ready == true`.
+5. If not ready: "There's nothing ready to harvest here."
+6. Check player has spade: "You need a spade to harvest."
+7. Check inventory space: need at least 1 free slot.
+8. Load seed def to get `FarmProduct`, `FarmMinYield`, `FarmMaxYield`, `FarmHarvestXP`.
+9. Start harvest action (3-tick duration):
+
+```go
+p.Action = &action.Action{
+ Type: "harvest",
+ TargetID: prefix,
+ TargetName: productName,
+ WaitLeft: 3,
+ Data: map[string]any{
+ "prefix": prefix,
+ "seed_id": seedID,
+ "product": seedDef.FarmProduct,
+ "min_yield": seedDef.FarmMinYield,
+ "max_yield": seedDef.FarmMaxYield,
+ "xp": seedDef.FarmHarvestXP,
+ },
+}
+p.ActionState = &ActionState{Type: ActionHarvesting, TargetName: "crops"}
+```
+
+10. In `advanceHarvest()`:
+ - Calculate yield: `minYield + rand.Intn(maxYield - minYield + 1)`. Bonus: `yield += farmingLevel / 20` (higher farming = slightly better yields).
+ - Cap yield by available inventory slots.
+ - Add items to inventory (stackable items merge, non-stackable use 1 slot each).
+ - Award XP: `harvestXP * yield`.
+ - Clear patch flags: set `prefix_seed = ""`, `prefix_stage = 0`, `prefix_ready = false`, `prefix_weeds = true` (weeds return after harvest).
+ - Save character.
+ - Output: "You harvest 7 guam leaves from the herb patch." with XP drop.
+
+### 4.3 `rake [patch]` (Active)
+
+**Classification:** Add `"rake"` to Active case in `classifyCommand()`.
+
+**Dispatch:**
+```go
+case "rake":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doRake(sess, "")
+ } else {
+ g.doRake(sess, strings.Join(args, " "))
+ }
+ return
+```
+
+**Handler: `doRake()`** in `cmd_farm.go`:
+
+1. Check player has a rake (tool_type "rake") in inventory or equipment.
+2. Find farming patches in room. If input given, match; otherwise find first patch with weeds or dead plants.
+3. Get flag prefix. Check `prefix_weeds == true` or `prefix_dead == true`.
+4. If neither: "The patch doesn't need raking."
+5. Start rake action (4-tick duration):
+
+```go
+p.Action = &action.Action{
+ Type: "rake",
+ TargetID: prefix,
+ TargetName: patchName,
+ WaitLeft: 4,
+ Data: map[string]any{
+ "prefix": prefix,
+ },
+}
+p.ActionState = &ActionState{Type: ActionRaking, TargetName: patchName}
+```
+
+6. In `advanceRake()`:
+ - If was dead: clear all flags (`prefix_seed = ""`, `prefix_dead = false`, `prefix_stage = 0`, `prefix_weeds = true`). Then set weeds false (raking clears both dead AND weeds in one go).
+ - Actually: set `prefix_weeds = false`, `prefix_dead = false`, `prefix_seed = ""`, `prefix_stage = 0`, `prefix_ready = false`, `prefix_diseased = false`.
+ - Raking gives **no farming XP** (as specified).
+ - Save character.
+ - Output: "You rake the patch clean."
+
+### 4.4 `water [patch]` (Active)
+
+**Classification:** Add `"water"` to Active case in `classifyCommand()`.
+
+**Dispatch:**
+```go
+case "water":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doWater(sess, "")
+ } else {
+ g.doWater(sess, strings.Join(args, " "))
+ }
+ return
+```
+
+**Handler: `doWater()`** in `cmd_farm.go`:
+
+1. Check player has a watering can (tool_type "watering_can") in inventory or equipment.
+2. Find farming patches in room. Match input or find first unwatered planted patch.
+3. Get flag prefix. Check there is a seed planted and it's not dead/ready.
+4. If `prefix_watered == true`: "The patch is already watered."
+5. If `prefix_ready == true`: "The crop is already fully grown."
+6. If no seed: "There's nothing planted here to water."
+7. Start water action (2-tick duration, fast):
+
+```go
+p.Action = &action.Action{
+ Type: "water",
+ TargetID: prefix,
+ TargetName: patchName,
+ WaitLeft: 2,
+ Data: map[string]any{
+ "prefix": prefix,
+ },
+}
+p.ActionState = &ActionState{Type: ActionWatering, TargetName: patchName}
+```
+
+8. In `advanceWater()`:
+ - Set `prefix_watered = true`.
+ - Save character.
+ - Output: "You water the herb patch."
+
+### 4.5 `cure [patch]` (Active)
+
+**Classification:** Add `"cure"` to Active case in `classifyCommand()`.
+
+**Dispatch:**
+```go
+case "cure":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doCure(sess, "")
+ } else {
+ g.doCure(sess, strings.Join(args, " "))
+ }
+ return
+```
+
+**Handler: `doCure()`** in `cmd_farm.go`:
+
+1. Check player has `plant_cure` item in inventory.
+2. Find farming patches in room. Match input or find first diseased patch.
+3. Get flag prefix. Check `prefix_diseased == true`.
+4. If not diseased: "The patch isn't diseased."
+5. Start cure action (2-tick duration):
+
+```go
+p.Action = &action.Action{
+ Type: "cure",
+ TargetID: prefix,
+ TargetName: patchName,
+ WaitLeft: 2,
+ Data: map[string]any{
+ "prefix": prefix,
+ },
+}
+p.ActionState = &ActionState{Type: ActionCuring, TargetName: patchName}
+```
+
+6. In `advanceCure()`:
+ - Remove 1 `plant_cure` from inventory.
+ - Set `prefix_diseased = false`.
+ - Save character.
+ - Output: "You apply the plant cure. The patch looks healthy again."
+
+### 4.6 `inspect [patch]` (Instant)
+
+**Classification:** Add `"inspect"` to Instant case in `classifyCommand()`.
+
+**Dispatch:**
+```go
+case "inspect":
+ if len(args) == 0 {
+ g.doInspect(sess, "")
+ } else {
+ g.doInspect(sess, strings.Join(args, " "))
+ }
+```
+
+**Handler: `doInspect()`** in `cmd_farm.go`:
+
+1. Find all farming patch objects in the current room.
+2. If none: "There are no farming patches here."
+3. If input is given, filter to matching patches.
+4. For each patch, load the player's flags and display:
+
+```
+=== Herb Patch 1 ===
+ Status: Growing (stage 2/4)
+ Planted: Guam seed
+ Watered: Yes
+ Diseased: No
+
+=== Herb Patch 2 ===
+ Status: Weeds
+ (Rake to clear before planting)
+
+=== Allotment Patch 1 ===
+ Status: Ready to harvest!
+ Planted: Potato seed
+```
+
+Possible statuses:
+- `Weeds` — needs raking
+- `Empty` — ready to plant
+- `Growing (stage N/M)` — in progress
+- `Watered` — growing and watered this stage
+- `Diseased!` — needs curing
+- `Dead` — needs raking
+- `Ready to harvest!` — fully grown
+
+---
+
+## 5. New Files to Create
+
+### Go Files
+
+| File | Purpose |
+|---|---|
+| `internal/game/cmd_farm.go` | Command handlers: `doPlant()`, `doHarvest()`, `doRake()`, `doWater()`, `doCure()`, `doInspect()` |
+| `internal/game/action_farm.go` | Action lifecycle: `advancePlant()`, `advanceHarvest()`, `advanceRake()`, `advanceWater()`, `advanceCure()`, `advanceFarmGrowth()`, `findActiveFarmPrefixes()`, `farmFlagPrefix()`, `ensureFarmState()`, `FarmTick()`, helper functions |
+
+### YAML Files
+
+**Items (seeds):**
+- `data/items/guam_seed.yaml`
+- `data/items/marrentill_seed.yaml`
+- `data/items/tarromin_seed.yaml`
+- `data/items/harralander_seed.yaml`
+- `data/items/ranarr_seed.yaml`
+- `data/items/toadflax_seed.yaml`
+- `data/items/irit_seed.yaml`
+- `data/items/avantoe_seed.yaml`
+- `data/items/kwuarm_seed.yaml`
+- `data/items/snapdragon_seed.yaml`
+- `data/items/cadantine_seed.yaml`
+- `data/items/lantadyme_seed.yaml`
+- `data/items/dwarf_weed_seed.yaml`
+- `data/items/torstol_seed.yaml`
+- `data/items/potato_seed.yaml`
+- `data/items/onion_seed.yaml`
+- `data/items/cabbage_seed.yaml`
+- `data/items/tomato_seed.yaml`
+- `data/items/sweetcorn_seed.yaml`
+- `data/items/strawberry_seed.yaml`
+- `data/items/watermelon_seed.yaml`
+
+**Items (products — only if they don't already exist):**
+- `data/items/guam_leaf.yaml`
+- `data/items/marrentill.yaml`
+- `data/items/tarromin.yaml`
+- `data/items/harralander.yaml`
+- `data/items/ranarr_weed.yaml`
+- `data/items/toadflax.yaml`
+- `data/items/irit_leaf.yaml`
+- `data/items/avantoe.yaml`
+- `data/items/kwuarm.yaml`
+- `data/items/snapdragon.yaml`
+- `data/items/cadantine.yaml`
+- `data/items/lantadyme.yaml`
+- `data/items/dwarf_weed.yaml`
+- `data/items/torstol.yaml`
+- `data/items/potato.yaml`
+- `data/items/onion.yaml`
+- `data/items/cabbage.yaml`
+- `data/items/tomato.yaml`
+- `data/items/sweetcorn.yaml` (ear_of_sweetcorn)
+- `data/items/strawberry.yaml`
+- `data/items/watermelon.yaml`
+
+**Items (tools):**
+- `data/items/rake.yaml`
+- `data/items/spade.yaml`
+- `data/items/watering_can.yaml`
+- `data/items/plant_cure.yaml`
+
+**Objects:**
+- `data/objects/herb_patch.yaml`
+- `data/objects/allotment_patch.yaml`
+- `data/objects/flower_patch.yaml`
+- `data/objects/tool_shed.yaml`
+
+**Rooms:**
+- `data/rooms/150.yaml` — Farming Hub (repurpose room 15 description or create new room)
+- `data/rooms/151.yaml` — Herb Garden
+- `data/rooms/152.yaml` — Allotment Field
+
+**Help Files:**
+- `data/help/plant.yaml`
+- `data/help/harvest.yaml`
+- `data/help/rake.yaml`
+- `data/help/water.yaml`
+- `data/help/cure.yaml`
+- `data/help/inspect.yaml`
+- `data/help/farming.yaml`
+
+---
+
+## 6. Code Changes to Existing Files
+
+### `internal/object/item.go`
+
+Add new fields to `ItemDef` struct:
+
+```go
+FarmPatchType string `yaml:"farm_patch_type"`
+FarmLevel int `yaml:"farm_level"`
+FarmPlantXP int `yaml:"farm_plant_xp"`
+FarmHarvestXP int `yaml:"farm_harvest_xp"`
+FarmStages int `yaml:"farm_stages"`
+FarmProduct string `yaml:"farm_product"`
+FarmMinYield int `yaml:"farm_min_yield"`
+FarmMaxYield int `yaml:"farm_max_yield"`
+```
+
+### `internal/game/game.go`
+
+**In `classifyCommand()`:**
+
+Add to Instant case:
+```go
+case "say", "score", "sc", "inventory", "i", "inv",
+ "look", "l", "exits", "help",
+ "map", "option", "options", "alias", "unalias",
+ "description", "desc", "queued", "color", "colors",
+ "colortable", "prompt", "style", "inspect": // <-- ADD inspect
+```
+
+Add to Active case:
+```go
+case "get", "take", "grab", "pick", "drop",
+ "attack", "kill",
+ "north", "n", "south", "s", "east", "e",
+ "west", "w", "up", "u", "down", "d",
+ "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft",
+ "plant", "harvest", "rake", "water", "cure": // <-- ADD THESE
+```
+
+**In `executeCommand()`:**
+
+Add cases for each farming command:
+```go
+case "plant":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ sess.WriteLine("Plant what?")
+ } else {
+ g.doPlant(sess, strings.Join(args, " "))
+ }
+ return
+case "harvest":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doHarvest(sess, "")
+ } else {
+ g.doHarvest(sess, strings.Join(args, " "))
+ }
+ return
+case "rake":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doRake(sess, "")
+ } else {
+ g.doRake(sess, strings.Join(args, " "))
+ }
+ return
+case "water":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doWater(sess, "")
+ } else {
+ g.doWater(sess, strings.Join(args, " "))
+ }
+ return
+case "cure":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doCure(sess, "")
+ } else {
+ g.doCure(sess, strings.Join(args, " "))
+ }
+ return
+case "inspect":
+ if len(args) == 0 {
+ g.doInspect(sess, "")
+ } else {
+ g.doInspect(sess, strings.Join(args, " "))
+ }
+```
+
+**In `Game` struct:**
+
+Add field:
+```go
+farmTickCounter int
+```
+
+### `internal/game/action_state.go`
+
+Add new ActionType constants:
+
+```go
+ActionPlanting ActionType = "planting"
+ActionHarvesting ActionType = "harvesting_crop"
+ActionRaking ActionType = "raking"
+ActionWatering ActionType = "watering"
+ActionCuring ActionType = "curing"
+```
+
+Add cases in `Description()`:
+
+```go
+case ActionPlanting:
+ return "planting " + a.TargetName
+case ActionHarvesting:
+ return "harvesting " + a.TargetName
+case ActionRaking:
+ return "raking a " + a.TargetName
+case ActionWatering:
+ return "watering a " + a.TargetName
+case ActionCuring:
+ return "curing a " + a.TargetName
+```
+
+### `internal/game/action.go`
+
+**In `AdvanceActions()`**, add cases for farming action types:
+
+```go
+case "plant":
+ g.advancePlant(sess, p)
+case "harvest":
+ g.advanceHarvest(sess, p)
+case "rake":
+ g.advanceRake(sess, p)
+case "water":
+ g.advanceWater(sess, p)
+case "cure":
+ g.advanceCure(sess, p)
+```
+
+**In `ProcessQueuedCommands()`**, add farming ActionTypes to the persistent list that should NOT be cleared after one tick:
+
+```go
+case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
+ ActionPlanting, ActionHarvesting, ActionRaking, ActionWatering, ActionCuring:
+ // keep these
+```
+
+### `cmd/mud/main.go`
+
+Add `g.FarmTick()` to the tick subscription (after `g.VisualTick()`).
+
+---
+
+## 7. Seeds
+
+### Herb Seeds
+
+All herb seeds: `stackable: true`, `farm_patch_type: "herb"`, `farm_stages: 4`.
+
+```yaml
+# data/items/guam_seed.yaml
+id: guam_seed
+name: guam seed
+color: "34"
+description: "A guam seed for planting in a herb patch."
+value: 1
+stackable: true
+farm_patch_type: herb
+farm_level: 9
+farm_plant_xp: 11
+farm_harvest_xp: 13
+farm_stages: 4
+farm_product: guam_leaf
+farm_min_yield: 3
+farm_max_yield: 12
+```
+
+```yaml
+# data/items/marrentill_seed.yaml
+id: marrentill_seed
+name: marrentill seed
+color: "34"
+description: "A marrentill seed for planting in a herb patch."
+value: 2
+stackable: true
+farm_patch_type: herb
+farm_level: 14
+farm_plant_xp: 14
+farm_harvest_xp: 15
+farm_stages: 4
+farm_product: marrentill
+farm_min_yield: 3
+farm_max_yield: 12
+```
+
+```yaml
+# data/items/tarromin_seed.yaml
+id: tarromin_seed
+name: tarromin seed
+color: "34"
+description: "A tarromin seed for planting in a herb patch."
+value: 3
+stackable: true
+farm_patch_type: herb
+farm_level: 19
+farm_plant_xp: 18
+farm_harvest_xp: 18
+farm_stages: 4
+farm_product: tarromin
+farm_min_yield: 3
+farm_max_yield: 12
+```
+
+```yaml
+# data/items/harralander_seed.yaml
+id: harralander_seed
+name: harralander seed
+color: "70"
+description: "A harralander seed for planting in a herb patch."
+value: 5
+stackable: true
+farm_patch_type: herb
+farm_level: 26
+farm_plant_xp: 22
+farm_harvest_xp: 24
+farm_stages: 4
+farm_product: harralander
+farm_min_yield: 3
+farm_max_yield: 12
+```
+
+```yaml
+# data/items/ranarr_seed.yaml
+id: ranarr_seed
+name: ranarr seed
+color: "28"
+description: "A ranarr seed for planting in a herb patch. Highly valued."
+value: 100
+stackable: true
+farm_patch_type: herb
+farm_level: 32
+farm_plant_xp: 27
+farm_harvest_xp: 31
+farm_stages: 4
+farm_product: ranarr_weed
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/toadflax_seed.yaml
+id: toadflax_seed
+name: toadflax seed
+color: "106"
+description: "A toadflax seed for planting in a herb patch."
+value: 50
+stackable: true
+farm_patch_type: herb
+farm_level: 38
+farm_plant_xp: 34
+farm_harvest_xp: 39
+farm_stages: 4
+farm_product: toadflax
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/irit_seed.yaml
+id: irit_seed
+name: irit seed
+color: "114"
+description: "An irit seed for planting in a herb patch."
+value: 40
+stackable: true
+farm_patch_type: herb
+farm_level: 44
+farm_plant_xp: 43
+farm_harvest_xp: 49
+farm_stages: 4
+farm_product: irit_leaf
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/avantoe_seed.yaml
+id: avantoe_seed
+name: avantoe seed
+color: "34"
+description: "An avantoe seed for planting in a herb patch."
+value: 60
+stackable: true
+farm_patch_type: herb
+farm_level: 50
+farm_plant_xp: 55
+farm_harvest_xp: 62
+farm_stages: 4
+farm_product: avantoe
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/kwuarm_seed.yaml
+id: kwuarm_seed
+name: kwuarm seed
+color: "178"
+description: "A kwuarm seed for planting in a herb patch."
+value: 80
+stackable: true
+farm_patch_type: herb
+farm_level: 56
+farm_plant_xp: 69
+farm_harvest_xp: 78
+farm_stages: 4
+farm_product: kwuarm
+farm_min_yield: 3
+farm_max_yield: 9
+```
+
+```yaml
+# data/items/snapdragon_seed.yaml
+id: snapdragon_seed
+name: snapdragon seed
+color: "161"
+description: "A snapdragon seed for planting in a herb patch."
+value: 150
+stackable: true
+farm_patch_type: herb
+farm_level: 62
+farm_plant_xp: 82
+farm_harvest_xp: 99
+farm_stages: 4
+farm_product: snapdragon
+farm_min_yield: 3
+farm_max_yield: 9
+```
+
+```yaml
+# data/items/cadantine_seed.yaml
+id: cadantine_seed
+name: cadantine seed
+color: "30"
+description: "A cadantine seed for planting in a herb patch."
+value: 120
+stackable: true
+farm_patch_type: herb
+farm_level: 67
+farm_plant_xp: 97
+farm_harvest_xp: 120
+farm_stages: 4
+farm_product: cadantine
+farm_min_yield: 3
+farm_max_yield: 9
+```
+
+```yaml
+# data/items/lantadyme_seed.yaml
+id: lantadyme_seed
+name: lantadyme seed
+color: "36"
+description: "A lantadyme seed for planting in a herb patch."
+value: 130
+stackable: true
+farm_patch_type: herb
+farm_level: 73
+farm_plant_xp: 105
+farm_harvest_xp: 135
+farm_stages: 4
+farm_product: lantadyme
+farm_min_yield: 3
+farm_max_yield: 8
+```
+
+```yaml
+# data/items/dwarf_weed_seed.yaml
+id: dwarf_weed_seed
+name: dwarf weed seed
+color: "100"
+description: "A dwarf weed seed for planting in a herb patch."
+value: 140
+stackable: true
+farm_patch_type: herb
+farm_level: 79
+farm_plant_xp: 120
+farm_harvest_xp: 150
+farm_stages: 4
+farm_product: dwarf_weed
+farm_min_yield: 3
+farm_max_yield: 8
+```
+
+```yaml
+# data/items/torstol_seed.yaml
+id: torstol_seed
+name: torstol seed
+color: "220"
+description: "A torstol seed for planting in a herb patch. Extremely rare and valuable."
+value: 500
+stackable: true
+farm_patch_type: herb
+farm_level: 85
+farm_plant_xp: 142
+farm_harvest_xp: 200
+farm_stages: 4
+farm_product: torstol
+farm_min_yield: 3
+farm_max_yield: 8
+```
+
+### Allotment Seeds
+
+All allotment seeds: `stackable: true`, `farm_patch_type: "allotment"`, `farm_stages: 4`.
+
+```yaml
+# data/items/potato_seed.yaml
+id: potato_seed
+name: potato seed
+color: "180"
+description: "A potato seed for planting in an allotment patch."
+value: 1
+stackable: true
+farm_patch_type: allotment
+farm_level: 1
+farm_plant_xp: 8
+farm_harvest_xp: 9
+farm_stages: 4
+farm_product: potato
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/onion_seed.yaml
+id: onion_seed
+name: onion seed
+color: "229"
+description: "An onion seed for planting in an allotment patch."
+value: 1
+stackable: true
+farm_patch_type: allotment
+farm_level: 5
+farm_plant_xp: 10
+farm_harvest_xp: 11
+farm_stages: 4
+farm_product: onion
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/cabbage_seed.yaml
+id: cabbage_seed
+name: cabbage seed
+color: "71"
+description: "A cabbage seed for planting in an allotment patch."
+value: 1
+stackable: true
+farm_patch_type: allotment
+farm_level: 7
+farm_plant_xp: 10
+farm_harvest_xp: 12
+farm_stages: 4
+farm_product: cabbage
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/tomato_seed.yaml
+id: tomato_seed
+name: tomato seed
+color: "196"
+description: "A tomato seed for planting in an allotment patch."
+value: 2
+stackable: true
+farm_patch_type: allotment
+farm_level: 12
+farm_plant_xp: 13
+farm_harvest_xp: 14
+farm_stages: 4
+farm_product: tomato
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/sweetcorn_seed.yaml
+id: sweetcorn_seed
+name: sweetcorn seed
+color: "220"
+description: "A sweetcorn seed for planting in an allotment patch."
+value: 5
+stackable: true
+farm_patch_type: allotment
+farm_level: 20
+farm_plant_xp: 17
+farm_harvest_xp: 19
+farm_stages: 4
+farm_product: sweetcorn
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/strawberry_seed.yaml
+id: strawberry_seed
+name: strawberry seed
+color: "197"
+description: "A strawberry seed for planting in an allotment patch."
+value: 8
+stackable: true
+farm_patch_type: allotment
+farm_level: 31
+farm_plant_xp: 26
+farm_harvest_xp: 29
+farm_stages: 4
+farm_product: strawberry
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+```yaml
+# data/items/watermelon_seed.yaml
+id: watermelon_seed
+name: watermelon seed
+color: "34"
+description: "A watermelon seed for planting in an allotment patch."
+value: 15
+stackable: true
+farm_patch_type: allotment
+farm_level: 47
+farm_plant_xp: 49
+farm_harvest_xp: 55
+farm_stages: 4
+farm_product: watermelon
+farm_min_yield: 3
+farm_max_yield: 10
+```
+
+---
+
+## 8. Tools
+
+### Rake
+
+```yaml
+# data/items/rake.yaml
+id: rake
+name: rake
+color: "94"
+description: "A sturdy rake for clearing weeds and dead plants from farming patches."
+value: 8
+stackable: false
+tool_type: rake
+```
+
+### Spade
+
+```yaml
+# data/items/spade.yaml
+id: spade
+name: spade
+color: "241"
+description: "A metal spade used for planting seeds and harvesting crops."
+value: 8
+stackable: false
+tool_type: spade
+```
+
+### Watering Can
+
+```yaml
+# data/items/watering_can.yaml
+id: watering_can
+name: watering can
+color: "39"
+description: "A watering can for hydrating farming patches. Watering eliminates disease risk."
+value: 12
+stackable: false
+tool_type: watering_can
+```
+
+### Plant Cure
+
+```yaml
+# data/items/plant_cure.yaml
+id: plant_cure
+name: plant cure
+color: "120"
+description: "A bio-engineered solution that cures diseased plants."
+value: 25
+stackable: true
+```
+
+### Tool Shed Object
+
+```yaml
+# data/objects/tool_shed.yaml
+id: tool_shed
+name: tool shed
+color: "94"
+behavior: ""
+hidden: false
+inroom_description: "A weathered tool shed stands against the wall."
+description: "A small shed for storing farming tools. Use 'use tool_shed' to store or retrieve tools."
+use_interactions:
+ - item: rake
+ message: "You store the rake in the tool shed."
+ action:
+ take_item: rake
+ set_player_flags:
+ tool_shed_rake: true
+ - item: spade
+ message: "You store the spade in the tool shed."
+ action:
+ take_item: spade
+ set_player_flags:
+ tool_shed_spade: true
+ - item: watering_can
+ message: "You store the watering can in the tool shed."
+ action:
+ take_item: watering_can
+ set_player_flags:
+ tool_shed_watering_can: true
+```
+
+**Retrieval:** For retrieving tools, the tool shed should also have interactions that are available when the player does NOT have the item but HAS the player flag. This requires adding conditional `use_interactions` on the object. Since the existing `UseInteraction` struct supports `Condition`, add retrieve entries:
+
+```yaml
+# Additional use_interactions on tool_shed.yaml
+ - item: ""
+ condition:
+ player_flag: tool_shed_rake
+ message: "You retrieve the rake from the tool shed."
+ action:
+ give_item: rake
+ set_player_flags:
+ tool_shed_rake: false
+ - item: ""
+ condition:
+ player_flag: tool_shed_spade
+ message: "You retrieve the spade from the tool shed."
+ action:
+ give_item: spade
+ set_player_flags:
+ tool_shed_spade: false
+ - item: ""
+ condition:
+ player_flag: tool_shed_watering_can
+ message: "You retrieve the watering can from the tool shed."
+ action:
+ give_item: watering_can
+ set_player_flags:
+ tool_shed_watering_can: false
+```
+
+**Note:** The current `UseInteraction` system requires an `item` field (the item being used on the object). For retrieval (no item needed), this may require either:
+1. A new `talk`-style behavior on the tool shed with dialog options, OR
+2. Adding a `talk` behavior that lists stored tools and lets the player choose, OR
+3. A new command `retrieve <tool> from shed`, OR
+4. Simply using `use shed` with no item to trigger a menu of stored tools.
+
+**Recommended approach:** Give the tool shed a `talk` behavior. When the player does `talk shed` or `use shed`, they get a dialog:
+
+```yaml
+# data/behaviors/tool_shed_talk.yaml
+id: tool_shed_talk
+type: talk
+nodes:
+ start:
+ message: "The tool shed is open. What would you like to do?"
+ options:
+ - text: "Store rake"
+ goto: store_rake
+ condition:
+ has_item: rake
+ - text: "Store spade"
+ goto: store_spade
+ condition:
+ has_item: spade
+ - text: "Store watering can"
+ goto: store_watering_can
+ condition:
+ has_item: watering_can
+ - text: "Retrieve rake"
+ goto: retrieve_rake
+ condition:
+ player_flag: tool_shed_rake
+ - text: "Retrieve spade"
+ goto: retrieve_spade
+ condition:
+ player_flag: tool_shed_spade
+ - text: "Retrieve watering can"
+ goto: retrieve_watering_can
+ condition:
+ player_flag: tool_shed_watering_can
+ - text: "Never mind"
+ end: true
+ store_rake:
+ message: "You store the rake in the tool shed."
+ action:
+ take_item: rake
+ set_player_flags:
+ tool_shed_rake: true
+ options:
+ - text: "Continue"
+ goto: start
+ store_spade:
+ message: "You store the spade in the tool shed."
+ action:
+ take_item: spade
+ set_player_flags:
+ tool_shed_spade: true
+ options:
+ - text: "Continue"
+ goto: start
+ store_watering_can:
+ message: "You store the watering can in the tool shed."
+ action:
+ take_item: watering_can
+ set_player_flags:
+ tool_shed_watering_can: true
+ options:
+ - text: "Continue"
+ goto: start
+ retrieve_rake:
+ message: "You retrieve the rake from the tool shed."
+ action:
+ give_item: rake
+ set_player_flags:
+ tool_shed_rake: false
+ options:
+ - text: "Continue"
+ goto: start
+ retrieve_spade:
+ message: "You retrieve the spade from the tool shed."
+ action:
+ give_item: spade
+ set_player_flags:
+ tool_shed_spade: false
+ options:
+ - text: "Continue"
+ goto: start
+ retrieve_watering_can:
+ message: "You retrieve the watering can from the tool shed."
+ action:
+ give_item: watering_can
+ set_player_flags:
+ tool_shed_watering_can: false
+ options:
+ - text: "Continue"
+ goto: start
+```
+
+Update `data/objects/tool_shed.yaml` to reference this behavior:
+
+```yaml
+id: tool_shed
+name: tool shed
+color: "94"
+behavior: tool_shed_talk
+hidden: false
+inroom_description: "A weathered tool shed stands against the wall."
+description: "A small shed for storing farming tools. Talk to it to store or retrieve tools."
+```
+
+---
+
+## 9. Plot Objects
+
+### Herb Patch
+
+```yaml
+# data/objects/herb_patch.yaml
+id: herb_patch
+name: herb patch
+color: "28"
+behavior: ""
+hidden: false
+inroom_description: "A prepared herb patch sits in the soil."
+description: "A small patch of tilled soil suitable for growing herbs. Use 'inspect' to check its status."
+```
+
+Note: `behavior: ""` because farming patches don't use the standard behavior system. They are interacted with via the dedicated farming commands (`plant`, `harvest`, `rake`, `water`, `cure`, `inspect`).
+
+### Allotment Patch
+
+```yaml
+# data/objects/allotment_patch.yaml
+id: allotment_patch
+name: allotment patch
+color: "94"
+behavior: ""
+hidden: false
+inroom_description: "An allotment patch is marked out in the ground."
+description: "A large patch of soil for growing vegetables. Use 'inspect' to check its status."
+```
+
+### Flower Patch
+
+```yaml
+# data/objects/flower_patch.yaml
+id: flower_patch
+name: flower patch
+color: "213"
+behavior: ""
+hidden: false
+inroom_description: "A flower patch is outlined with small stones."
+description: "A small decorative patch for growing flowers. Use 'inspect' to check its status."
+```
+
+### How `look` Shows Patch State
+
+The `doLook` / `doLookTarget` functions need modification. When a player looks at a farming patch object, the system should append the player's personal patch state to the object description.
+
+**In `doLookTarget()` (likely in `cmd_look.go` or the look handler):**
+
+When the target matches a farming patch object, after displaying the base description, append:
+
+```go
+func (g *Game) farmPatchLookSuffix(p *player.Player, defID string, index int) string {
+ prefix := farmFlagPrefix(defID, index)
+ if prefix == "" {
+ return ""
+ }
+ g.ensureFarmState(p, prefix)
+
+ weeds, _ := p.Flags[prefix+"_weeds"].(bool)
+ if weeds {
+ return "\nIt is overgrown with weeds."
+ }
+
+ seedID, _ := p.Flags[prefix+"_seed"].(string)
+ if seedID == "" {
+ return "\nThe patch is empty and ready for planting."
+ }
+
+ dead, _ := p.Flags[prefix+"_dead"].(bool)
+ if dead {
+ return "\nThe plant has died. You need to rake it clean."
+ }
+
+ diseased, _ := p.Flags[prefix+"_diseased"].(bool)
+ if diseased {
+ return "\nThe plant looks diseased! Use plant cure to save it."
+ }
+
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if ready {
+ seedDef, _ := g.ItemStore.Load(seedID)
+ name := seedID
+ if seedDef != nil {
+ name = seedDef.Name
+ }
+ return fmt.Sprintf("\nA fully grown %s is ready to harvest!", name)
+ }
+
+ stage, _ := p.Flags[prefix+"_stage"].(int)
+ seedDef, _ := g.ItemStore.Load(seedID)
+ maxStages := 4
+ if seedDef != nil && seedDef.FarmStages > 0 {
+ maxStages = seedDef.FarmStages
+ }
+ name := seedID
+ if seedDef != nil {
+ name = seedDef.Name
+ }
+ watered, _ := p.Flags[prefix+"_watered"].(bool)
+ suffix := fmt.Sprintf("\nA %s is growing (stage %d/%d).", name, stage, maxStages)
+ if watered {
+ suffix += " It has been watered."
+ }
+ return suffix
+}
+```
+
+The room `look` listing should also reflect patch state. In the objects section of `doLook`, for each farming patch object, replace the generic `inroom_description` with a state-aware version:
+
+- Weeds: "A herb patch sits here, overgrown with weeds."
+- Empty: "An empty herb patch is ready for planting."
+- Growing: "A herb patch has a small plant growing in it."
+- Diseased: "A herb patch has a {196}sickly-looking{/} plant in it."
+- Dead: "A herb patch contains a dead, withered plant."
+- Ready: "A herb patch has a {34 bold}fully grown crop{/} ready to harvest!"
+
+---
+
+## 10. Rooms
+
+### Room Layout
+
+Room 15 currently is "Alchemy Lab" with exits east:16, west:14. We will repurpose it as the farming hub entrance, or more practically, create new rooms at the end of the room list (rooms 150-152) and connect them.
+
+**Connection point:** Add a south exit from an existing hub room to room 150. Room 1 (Town Square) or another central room should connect to the farming area.
+
+Alternatively, give room 15 (Alchemy Lab) a south exit to room 150.
+
+### Room 150: Farming Hub
+
+```yaml
+# data/rooms/150.yaml
+id: 150
+name: "Hydroponics Bay"
+description: "A large enclosed area with climate-controlled growing stations. {34}Lush green patches{/} of soil are laid out in neat rows under artificial UV lamps. A {94}tool shed{/} stands near the entrance."
+map_symbol: "H"
+exits:
+ north: 15
+ east: 151
+ south: 152
+objects:
+ - id: tool_shed
+spawns:
+ - item_id: rake
+ quantity: 1
+ respawn_ticks: 120
+ - item_id: spade
+ quantity: 1
+ respawn_ticks: 120
+ - item_id: watering_can
+ quantity: 1
+ respawn_ticks: 120
+ - item_id: plant_cure
+ quantity: 3
+ respawn_ticks: 200
+ - item_id: guam_seed
+ quantity: 5
+ respawn_ticks: 300
+ - item_id: potato_seed
+ quantity: 10
+ respawn_ticks: 300
+ - item_id: onion_seed
+ quantity: 10
+ respawn_ticks: 300
+```
+
+### Room 151: Herb Garden
+
+```yaml
+# data/rooms/151.yaml
+id: 151
+name: "Herb Garden"
+description: "A dedicated section of the hydroponics bay for growing herbs. {28}Herb patches{/} are arranged in two rows under specialized grow lights."
+map_symbol: "G"
+exits:
+ west: 150
+objects:
+ - id: herb_patch
+ - id: herb_patch
+ - id: herb_patch
+ - id: herb_patch
+```
+
+### Room 152: Allotment Field
+
+```yaml
+# data/rooms/152.yaml
+id: 152
+name: "Allotment Field"
+description: "A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps."
+map_symbol: "A"
+exits:
+ north: 150
+objects:
+ - id: allotment_patch
+ - id: allotment_patch
+ - id: allotment_patch
+ - id: flower_patch
+```
+
+### Connect Room 15 to Room 150
+
+Update `data/rooms/15.yaml` to add a south exit:
+
+```yaml
+id: 15
+name: "Alchemy Lab"
+description: "Shelves lined with glass vials, dried herbs, and bubbling cauldrons. This area is not yet accessible."
+exits:
+ east: 16
+ west: 14
+ south: 150
+```
+
+---
+
+## 11. Growth Stages
+
+### Stage Definitions
+
+Each crop type has a fixed number of growth stages. Growth advances by 1 stage per farm tick (every 500 game ticks ≈ 5 minutes).
+
+| Crop Type | Stages | Total Grow Time (online) |
+|---|---|---|
+| Herb | 4 | ~20 minutes |
+| Allotment | 4 | ~20 minutes |
+| Flower | 3 | ~15 minutes |
+| Bush | 5 | ~25 minutes |
+| Tree | 6 | ~30 minutes |
+
+All stages are equal duration (1 farm tick each). The `farm_stages` field on the seed item controls this per-seed.
+
+### Stage Progression
+
+```
+Stage 0: Just planted (seedling)
+Stage 1: Small sprout
+Stage 2: Growing plant
+Stage 3: Maturing plant
+Stage 4: Fully grown (for 4-stage crops)
+```
+
+At each stage transition, the disease check occurs (unless watered). Watering is reset each stage — you must water each stage individually if you want full protection.
+
+### Watering Reset Mechanic
+
+When growth advances a stage:
+1. The `_watered` flag is set to `false`.
+2. Disease check runs (10% chance if not watered, 0% if watered).
+3. Player must water again for the next stage.
+
+This means a 4-stage herb crop with full watering protection requires 4 watering actions spread across 4 farm ticks.
+
+---
+
+## 12. Disease & Death
+
+### Disease Chance
+
+- **Base disease chance per growth stage:** 10% (0.10)
+- **If watered:** 0% (eliminated entirely)
+- **Not affected by farming level** (keep it simple for Phase 1)
+
+### Disease Flow
+
+```
+[Growth tick fires]
+ |
+ +-- Is plant diseased?
+ | |
+ | +-- YES: Plant DIES. Set _dead = true, _diseased = false.
+ | | Notify player: "Your <seed> has died from disease!"
+ | |
+ | +-- NO: Continue to growth check.
+ |
+ +-- Advance stage by 1.
+ |
+ +-- Is plant now fully grown?
+ | |
+ | +-- YES: Set _ready = true. Notify: "Your <seed> is fully grown!"
+ | |
+ | +-- NO: Roll disease check.
+ | |
+ | +-- Watered? No disease.
+ | |
+ | +-- Not watered? 10% chance of disease.
+ | |
+ | +-- Disease! Set _diseased = true. Notify: "Your <seed> has become diseased!"
+ | |
+ | +-- No disease. Continue growing.
+ |
+ +-- Reset _watered to false.
+```
+
+### Curing
+
+- Use `cure` command or `use plant_cure on herb patch`.
+- Consumes 1 `plant_cure` from inventory.
+- Sets `_diseased = false`.
+- Must be done **before the next farm tick** or the plant dies.
+
+### Dead Plants
+
+- Dead plants block the patch. Nothing can be planted.
+- Must be cleared with `rake` command.
+- Raking dead plants awards **no XP**.
+- After raking, the patch has weeds (must be raked again to clear before planting).
+- **Actually, simplify:** Raking a dead plant fully clears the patch (no double-rake). Set `_weeds = false`, `_dead = false`, `_seed = ""`, etc. The patch is immediately ready for replanting.
+
+### Raking Weeds
+
+- New patches start with weeds.
+- After harvesting, the patch returns to having weeds.
+- Raking weeds also awards **no XP** and takes 4 ticks.
+- After raking weeds, patch is empty and ready for planting.
+
+---
+
+## 13. Harvesting
+
+### Harvest Mechanics
+
+1. Player must be in a room with a farming patch that has `_ready == true`.
+2. Player must have a spade (tool_type "spade") in inventory or equipment.
+3. Player must have at least 1 free inventory slot.
+4. Harvest action takes 3 ticks.
+
+### Yield Calculation
+
+```go
+func farmYield(minYield, maxYield, farmingLevel int) int {
+ baseYield := minYield + rand.Intn(maxYield-minYield+1)
+ bonus := farmingLevel / 20
+ return baseYield + bonus
+}
+```
+
+- Herbs: base 3-12, +1 per 20 farming levels → at level 99: 3-17
+- Allotments: base 3-10, +1 per 20 farming levels → at level 99: 3-14
+
+### Yield Capping
+
+Yield is capped by available inventory space. If the product is stackable (herbs could be), all yield goes into one slot. If not stackable, yield = min(yield, freeSlots).
+
+For simplicity, **all farm products are stackable** (herb leaves, vegetables).
+
+### Harvest XP
+
+XP is awarded per harvest action (not per item). The total XP is `farm_harvest_xp` from the seed def, awarded once when harvesting completes.
+
+### Post-Harvest
+
+After harvesting:
+- `_seed = ""`, `_stage = 0`, `_ready = false`, `_watered = false`
+- `_weeds = true` — weeds grow back after harvest (must rake before replanting)
+
+### Output Messages
+
+```
+"You harvest 7 guam leaves from the herb patch."
+" (+13xp frm)" // if xp_drops enabled
+```
+
+---
+
+## 14. XP Table
+
+### Herb Seeds
+
+| Seed | Level | Plant XP | Harvest XP | Total XP (min harvest) |
+|---|---|---|---|---|
+| Guam | 9 | 11 | 13 | 24 |
+| Marrentill | 14 | 14 | 15 | 29 |
+| Tarromin | 19 | 18 | 18 | 36 |
+| Harralander | 26 | 22 | 24 | 46 |
+| Ranarr | 32 | 27 | 31 | 58 |
+| Toadflax | 38 | 34 | 39 | 73 |
+| Irit | 44 | 43 | 49 | 92 |
+| Avantoe | 50 | 55 | 62 | 117 |
+| Kwuarm | 56 | 69 | 78 | 147 |
+| Snapdragon | 62 | 82 | 99 | 181 |
+| Cadantine | 67 | 97 | 120 | 217 |
+| Lantadyme | 73 | 105 | 135 | 240 |
+| Dwarf Weed | 79 | 120 | 150 | 270 |
+| Torstol | 85 | 142 | 200 | 342 |
+
+### Allotment Seeds
+
+| Seed | Level | Plant XP | Harvest XP | Total XP |
+|---|---|---|---|---|
+| Potato | 1 | 8 | 9 | 17 |
+| Onion | 5 | 10 | 11 | 21 |
+| Cabbage | 7 | 10 | 12 | 22 |
+| Tomato | 12 | 13 | 14 | 27 |
+| Sweetcorn | 20 | 17 | 19 | 36 |
+| Strawberry | 31 | 26 | 29 | 55 |
+| Watermelon | 47 | 49 | 55 | 104 |
+
+### XP Comparison to Other Skills
+
+These values are intentionally lower per-action than gathering skills because farming is passive — the player plants, waters, and waits. The time-gated nature means farming XP comes slowly but with minimal active effort.
+
+---
+
+## 15. Tool Shed
+
+### Overview
+
+The tool shed is an object in the Farming Hub room (150). Players can interact with it to store and retrieve farming tools, freeing up inventory space while farming.
+
+### Implementation
+
+The tool shed uses the existing **talk behavior** system. When a player does `talk shed` or `talk tool_shed`, they enter a dialog that shows available storage/retrieval options based on their inventory and player flags.
+
+**Player flags for tool shed:**
+- `tool_shed_rake: true/false`
+- `tool_shed_spade: true/false`
+- `tool_shed_watering_can: true/false`
+
+**Behavior YAML:** See Section 8 above (`data/behaviors/tool_shed_talk.yaml`).
+
+**Object YAML:** See Section 8 above (`data/objects/tool_shed.yaml`).
+
+### Tool Shed in `look`
+
+When the player looks at the tool shed, the description should mention what's stored:
+
+```go
+func (g *Game) toolShedLookSuffix(p *player.Player) string {
+ var stored []string
+ if v, _ := p.Flags["tool_shed_rake"].(bool); v {
+ stored = append(stored, "a rake")
+ }
+ if v, _ := p.Flags["tool_shed_spade"].(bool); v {
+ stored = append(stored, "a spade")
+ }
+ if v, _ := p.Flags["tool_shed_watering_can"].(bool); v {
+ stored = append(stored, "a watering can")
+ }
+ if len(stored) == 0 {
+ return "\nThe shed is empty."
+ }
+ return fmt.Sprintf("\nInside: %s.", strings.Join(stored, ", "))
+}
+```
+
+This could be appended when the player does `look tool shed`. Since the description field supports `{quality}` placeholder for fire objects, a similar approach could be used, but it's simpler to handle this in the Go code for the `doLookTarget` handler.
+
+---
+
+## 16. Help Files
+
+### `data/help/plant.yaml`
+
+```yaml
+name: "plant"
+category: "Skills"
+description: |
+ Plant a seed in a farming patch.
+
+ Usage: plant <seed>
+
+ Requires a spade in your inventory. The seed must match the
+ patch type in the current room (herb seeds go in herb patches,
+ vegetable seeds go in allotment patches).
+
+ The patch must be clear of weeds (use 'rake' first) and empty.
+
+ Planting awards a small amount of farming XP.
+
+ See also: help farming, help harvest, help rake, help water
+```
+
+### `data/help/harvest.yaml`
+
+```yaml
+name: "harvest"
+category: "Skills"
+description: |
+ Harvest a fully grown crop from a farming patch.
+
+ Usage: harvest [patch]
+
+ Requires a spade in your inventory. The crop must be fully
+ grown (check with 'inspect'). Yields a random amount of the
+ crop based on your farming level.
+
+ If no patch is specified, the first harvestable patch in the
+ room is targeted.
+
+ After harvesting, the patch grows weeds and must be raked
+ before replanting.
+
+ See also: help farming, help plant, help inspect
+```
+
+### `data/help/rake.yaml`
+
+```yaml
+name: "rake"
+category: "Skills"
+description: |
+ Clear weeds or dead plants from a farming patch.
+
+ Usage: rake [patch]
+
+ Requires a rake in your inventory. Patches start with weeds
+ and regrow weeds after harvesting. Dead plants from disease
+ also need to be raked before replanting.
+
+ Raking awards no farming XP.
+
+ If no patch is specified, the first patch needing raking
+ in the room is targeted.
+
+ See also: help farming, help plant, help water
+```
+
+### `data/help/water.yaml`
+
+```yaml
+name: "water"
+category: "Skills"
+description: |
+ Water a farming patch to prevent disease.
+
+ Usage: water [patch]
+
+ Requires a watering can in your inventory. Watering a patch
+ eliminates the disease chance for the current growth stage.
+ You must water each stage separately as the watering resets
+ when the plant grows.
+
+ If no patch is specified, the first unwatered patch with a
+ growing plant is targeted.
+
+ See also: help farming, help plant, help cure
+```
+
+### `data/help/cure.yaml`
+
+```yaml
+name: "cure"
+category: "Skills"
+description: |
+ Cure a diseased farming patch.
+
+ Usage: cure [patch]
+
+ Requires a plant cure item in your inventory (consumed on use).
+ Diseased plants must be cured before the next growth tick or
+ they will die.
+
+ If no patch is specified, the first diseased patch in the
+ room is targeted.
+
+ See also: help farming, help water, help inspect
+```
+
+### `data/help/inspect.yaml`
+
+```yaml
+name: "inspect"
+category: "Skills"
+description: |
+ Check the status of farming patches in the current room.
+
+ Usage: inspect [patch]
+
+ Shows the current state of each farming patch including:
+ - What is planted
+ - Growth stage
+ - Whether it has been watered
+ - Disease status
+ - Whether it is ready to harvest
+
+ If no patch is specified, all patches in the room are shown.
+
+ See also: help farming, help plant, help harvest
+```
+
+### `data/help/farming.yaml`
+
+```yaml
+name: "farming"
+category: "Skills"
+description: |
+ Farming lets you grow herbs and vegetables in patches.
+
+ The basic cycle:
+ 1. Rake the patch to clear weeds (requires rake)
+ 2. Plant a seed (requires spade, seed in inventory)
+ 3. Water the patch to prevent disease (requires watering can)
+ 4. Wait for the plant to grow through stages
+ 5. Water again at each growth stage for protection
+ 6. Harvest the fully grown crop (requires spade)
+
+ Growth happens every ~5 minutes while you are online.
+ Crops do not grow while you are logged off.
+
+ Disease: Each growth stage has a 10% chance of disease unless
+ the patch was watered. Diseased plants can be cured with plant
+ cure. If not cured before the next growth tick, the plant dies
+ and must be raked away.
+
+ Tool shed: Store farming tools to free inventory space.
+ Use 'talk shed' to store or retrieve tools.
+
+ Patches: Herb patches grow herb seeds. Allotment patches grow
+ vegetable seeds. Check 'inspect' for patch status.
+
+ Commands: plant, harvest, rake, water, cure, inspect
+
+ See also: help plant, help harvest, help rake, help water,
+ help cure, help inspect
+```
+
+---
+
+## Implementation Order
+
+Recommended order for implementing this feature:
+
+1. **Add `ItemDef` fields** (`internal/object/item.go`) — the 8 new `farm_*` fields. Run `make vet`.
+2. **Add `ActionType` constants** (`internal/game/action_state.go`) — 5 new types + `Description()` cases.
+3. **Create `action_farm.go`** — farm state helpers, `ensureFarmState()`, `farmFlagPrefix()`, `findActiveFarmPrefixes()`, `FarmTick()`, `advanceFarmGrowth()`, and all `advanceXxx()` functions.
+4. **Create `cmd_farm.go`** — all command handlers: `doPlant()`, `doHarvest()`, `doRake()`, `doWater()`, `doCure()`, `doInspect()`, and look suffix helpers.
+5. **Update `game.go`** — `classifyCommand()`, `executeCommand()`, `farmTickCounter` field on `Game`.
+6. **Update `action.go`** — add farming cases to `AdvanceActions()` and `ProcessQueuedCommands()`.
+7. **Update `main.go`** — add `g.FarmTick()` to tick subscription.
+8. **Create tool YAML files** — rake, spade, watering_can, plant_cure in `data/items/`.
+9. **Create seed YAML files** — all 21 seeds in `data/items/`.
+10. **Create product YAML files** — all harvest products that don't already exist.
+11. **Create object YAML files** — herb_patch, allotment_patch, flower_patch, tool_shed in `data/objects/`.
+12. **Create behavior YAML** — `tool_shed_talk` in `data/behaviors/`.
+13. **Create room YAML files** — rooms 150, 151, 152 in `data/rooms/`.
+14. **Update room 15** — add south exit to room 150.
+15. **Create help YAML files** — all 7 help files.
+16. **Integrate patch state into look** — modify `doLookTarget()` and room look to show per-player patch state.
+17. **Test** — `make build && make test && make vet`.
+
+---
+
+## Edge Cases & Notes
+
+- **Inventory full when harvesting:** Cap yield at available slots. If no slots available, "Your inventory is too full!"
+- **No patches in room:** "There are no farming patches here."
+- **Wrong seed for patch:** "That seed can't be planted in this type of patch."
+- **Already planted:** "Something is already growing in this patch."
+- **No tool:** "You need a rake/spade/watering can to do that."
+- **No plant cure:** "You don't have any plant cure."
+- **Multi-patch rooms:** Support numbered targeting like `plant guam seed 2.patch` or implicit "first available" logic.
+- **Cancellation:** All farming actions (plant, harvest, rake, water, cure) are cancelled by movement, combat, or other active commands (standard `CancelAction` behavior).
+- **Offline growth:** Intentionally disabled. Crops freeze when player logs off. This means farming is most effective when the player stays online and periodically waters/harvests.
+- **Multiple players:** Since state is per-player (player flags), two players can farm the same patches independently. Each sees their own state.
+- **Save frequency:** Character is saved after every state change (planting, watering, curing, harvesting, raking, growth tick). This matches the existing pattern of `g.AccountStore.SaveCharacter(p)` after mutations.
+- **Color targets:** Add `farm_grow` and `farm_disease` to the color system (in `config/colors.go` or wherever color targets are registered). Suggested defaults: `farm_grow: "34"` (green), `farm_disease: "196"` (red).
+- **Player flag cleanup:** Dead characters or deleted characters will have farming flags in their YAML files. This is harmless — the flags are simply ignored if the character is deleted. No cleanup needed.
+- **Flag type safety:** When reading flags, always use type assertions with default values: `stage, _ := p.Flags[prefix+"_stage"].(int)`. YAML deserialization may store ints as `int` or `float64` depending on the value — handle both like `OptionInt()` does.