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