# 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 ` | 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 ` (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 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 has died from disease!" | | | +-- NO: Continue to growth check. | +-- Advance stage by 1. | +-- Is plant now fully grown? | | | +-- YES: Set _ready = true. Notify: "Your is fully grown!" | | | +-- NO: Roll disease check. | | | +-- Watered? No disease. | | | +-- Not watered? 10% chance of disease. | | | +-- Disease! Set _diseased = true. Notify: "Your 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 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.