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