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