aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md2
-rw-r--r--cmd/mud/main.go1
-rw-r--r--data/behaviors/tool_shed_talk.yaml86
-rw-r--r--data/help/cure.yaml15
-rw-r--r--data/help/farming.yaml31
-rw-r--r--data/help/harvest.yaml18
-rw-r--r--data/help/inspect.yaml17
-rw-r--r--data/help/plant.yaml16
-rw-r--r--data/help/rake.yaml17
-rw-r--r--data/help/water.yaml16
-rw-r--r--data/items/avantoe.yaml2
-rw-r--r--data/items/avantoe_seed.yaml14
-rw-r--r--data/items/cabbage.yaml8
-rw-r--r--data/items/cabbage_seed.yaml14
-rw-r--r--data/items/cadantine.yaml2
-rw-r--r--data/items/cadantine_seed.yaml14
-rw-r--r--data/items/dwarf_weed.yaml2
-rw-r--r--data/items/dwarf_weed_seed.yaml14
-rw-r--r--data/items/guam.yaml2
-rw-r--r--data/items/guam_seed.yaml14
-rw-r--r--data/items/harralander.yaml2
-rw-r--r--data/items/harralander_seed.yaml14
-rw-r--r--data/items/irit.yaml2
-rw-r--r--data/items/irit_seed.yaml14
-rw-r--r--data/items/kwuarm.yaml2
-rw-r--r--data/items/kwuarm_seed.yaml14
-rw-r--r--data/items/lantadyme.yaml2
-rw-r--r--data/items/lantadyme_seed.yaml14
-rw-r--r--data/items/marrentill.yaml2
-rw-r--r--data/items/marrentill_seed.yaml14
-rw-r--r--data/items/onion.yaml8
-rw-r--r--data/items/onion_seed.yaml12
-rw-r--r--data/items/plant_cure.yaml6
-rw-r--r--data/items/potato.yaml8
-rw-r--r--data/items/potato_seed.yaml10
-rw-r--r--data/items/rake.yaml7
-rw-r--r--data/items/ranarr.yaml2
-rw-r--r--data/items/ranarr_seed.yaml10
-rw-r--r--data/items/snapdragon.yaml2
-rw-r--r--data/items/snapdragon_seed.yaml16
-rw-r--r--data/items/spade.yaml7
-rw-r--r--data/items/strawberry.yaml8
-rw-r--r--data/items/strawberry_seed.yaml10
-rw-r--r--data/items/sweetcorn.yaml8
-rw-r--r--data/items/sweetcorn_seed.yaml10
-rw-r--r--data/items/tarromin.yaml2
-rw-r--r--data/items/tarromin_seed.yaml14
-rw-r--r--data/items/toadflax.yaml2
-rw-r--r--data/items/toadflax_seed.yaml14
-rw-r--r--data/items/tomato.yaml8
-rw-r--r--data/items/tomato_seed.yaml12
-rw-r--r--data/items/torstol.yaml2
-rw-r--r--data/items/torstol_seed.yaml10
-rw-r--r--data/items/watering_can.yaml7
-rw-r--r--data/items/watermelon.yaml8
-rw-r--r--data/items/watermelon_seed.yaml10
-rw-r--r--data/objects/allotment_patch.yaml7
-rw-r--r--data/objects/flower_patch.yaml7
-rw-r--r--data/objects/herb_patch.yaml7
-rw-r--r--data/objects/tool_shed.yaml7
-rw-r--r--data/rooms/15.yaml1
-rw-r--r--data/rooms/150.yaml32
-rw-r--r--data/rooms/151.yaml11
-rw-r--r--data/rooms/152.yaml11
-rw-r--r--internal/config/config.go2
-rw-r--r--internal/game/action.go10
-rw-r--r--internal/game/action_farm.go657
-rw-r--r--internal/game/action_state.go15
-rw-r--r--internal/game/cmd_farm.go523
-rw-r--r--internal/game/cmd_look.go11
-rw-r--r--internal/game/game.go54
-rw-r--r--internal/object/item.go8
72 files changed, 1926 insertions, 35 deletions
diff --git a/TODO.md b/TODO.md
index 61d2738..98f19fc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -45,7 +45,7 @@ See `skill_plans/pharmacy.md`. Herb cleaning and potion mixing. Can work initial
See `skill_plans/farming.md`. Grows herbs for Pharmacy and allotment crops. Per-player plot state via player flags.
-- [ ] **Farming Skill** — Add `plant`, `harvest`, `rake`, `water`, `cure`, `inspect` commands. Farming state tracked per-player via player flags (each plot stores seed, growth stage, watered, diseased status). Add `FarmTick()` that runs every ~500 ticks for online players: advances growth stages, rolls disease chance (watering prevents), diseased plants die if not cured. Create seed items (14 herb seeds, 7 allotment seeds). Create farming tools (rake, spade, watering_can, plant_cure). Create plot objects (herb_patch, allotment_patch). Create tool_shed object with store/retrieve interaction. Harvesting gives 3-15 herbs or 3-10 crops based on farming level. Create farming area rooms south of room 15.
+- [x] **Farming Skill** — Add `plant`, `harvest`, `rake`, `water`, `cure`, `inspect` commands. Farming state tracked per-player via player flags (each plot stores seed, growth stage, watered, diseased status). Add `FarmTick()` that runs every ~500 ticks for online players: advances growth stages, rolls disease chance (watering prevents), diseased plants die if not cured. Create seed items (14 herb seeds, 7 allotment seeds). Create farming tools (rake, spade, watering_can, plant_cure). Create plot objects (herb_patch, allotment_patch, flower_patch). Create tool_shed object with talk behavior for store/retrieve interaction. Harvesting gives 3-15 herbs or 3-10 crops based on farming level. Create farming area rooms (150-152) south of room 15.
---
diff --git a/cmd/mud/main.go b/cmd/mud/main.go
index 82427ba..55e3bbf 100644
--- a/cmd/mud/main.go
+++ b/cmd/mud/main.go
@@ -55,6 +55,7 @@ func main() {
g.BroadcastRespawns()
g.VisualTick()
g.SneakTick()
+ g.FarmTick()
return true
})
diff --git a/data/behaviors/tool_shed_talk.yaml b/data/behaviors/tool_shed_talk.yaml
new file mode 100644
index 0000000..87db9fa
--- /dev/null
+++ b/data/behaviors/tool_shed_talk.yaml
@@ -0,0 +1,86 @@
+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
diff --git a/data/help/cure.yaml b/data/help/cure.yaml
new file mode 100644
index 0000000..3bb7bc3
--- /dev/null
+++ b/data/help/cure.yaml
@@ -0,0 +1,15 @@
+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
diff --git a/data/help/farming.yaml b/data/help/farming.yaml
new file mode 100644
index 0000000..2dd2ff2
--- /dev/null
+++ b/data/help/farming.yaml
@@ -0,0 +1,31 @@
+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
diff --git a/data/help/harvest.yaml b/data/help/harvest.yaml
new file mode 100644
index 0000000..88fa19e
--- /dev/null
+++ b/data/help/harvest.yaml
@@ -0,0 +1,18 @@
+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
diff --git a/data/help/inspect.yaml b/data/help/inspect.yaml
new file mode 100644
index 0000000..f81dc6b
--- /dev/null
+++ b/data/help/inspect.yaml
@@ -0,0 +1,17 @@
+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
diff --git a/data/help/plant.yaml b/data/help/plant.yaml
new file mode 100644
index 0000000..4b807ea
--- /dev/null
+++ b/data/help/plant.yaml
@@ -0,0 +1,16 @@
+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
diff --git a/data/help/rake.yaml b/data/help/rake.yaml
new file mode 100644
index 0000000..bf58da7
--- /dev/null
+++ b/data/help/rake.yaml
@@ -0,0 +1,17 @@
+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
diff --git a/data/help/water.yaml b/data/help/water.yaml
new file mode 100644
index 0000000..e778eb2
--- /dev/null
+++ b/data/help/water.yaml
@@ -0,0 +1,16 @@
+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
diff --git a/data/items/avantoe.yaml b/data/items/avantoe.yaml
index 709b583..d596096 100644
--- a/data/items/avantoe.yaml
+++ b/data/items/avantoe.yaml
@@ -3,4 +3,4 @@ name: "avantoe"
color: "48"
description: "A clean avantoe leaf, ready for use in pharmacy."
value: 48
-stackable: false
+stackable: true
diff --git a/data/items/avantoe_seed.yaml b/data/items/avantoe_seed.yaml
new file mode 100644
index 0000000..fb7b427
--- /dev/null
+++ b/data/items/avantoe_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/cabbage.yaml b/data/items/cabbage.yaml
new file mode 100644
index 0000000..c04fd4e
--- /dev/null
+++ b/data/items/cabbage.yaml
@@ -0,0 +1,8 @@
+id: cabbage
+name: cabbage
+color: "71"
+description: "A freshly harvested cabbage."
+value: 3
+stackable: true
+heal_value: 1
+eat_message: "You eat the cabbage."
diff --git a/data/items/cabbage_seed.yaml b/data/items/cabbage_seed.yaml
index 35ebee3..8a6db33 100644
--- a/data/items/cabbage_seed.yaml
+++ b/data/items/cabbage_seed.yaml
@@ -1,6 +1,14 @@
id: cabbage_seed
name: cabbage seed
-color: "34"
+color: "71"
description: "A seed for growing cabbages."
-value: 4
-stackable: true \ No newline at end of file
+value: 2
+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 \ No newline at end of file
diff --git a/data/items/cadantine.yaml b/data/items/cadantine.yaml
index c422bf6..f684f0d 100644
--- a/data/items/cadantine.yaml
+++ b/data/items/cadantine.yaml
@@ -3,4 +3,4 @@ name: "cadantine"
color: "48"
description: "A clean cadantine leaf, ready for use in pharmacy."
value: 70
-stackable: false
+stackable: true
diff --git a/data/items/cadantine_seed.yaml b/data/items/cadantine_seed.yaml
new file mode 100644
index 0000000..8326033
--- /dev/null
+++ b/data/items/cadantine_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/dwarf_weed.yaml b/data/items/dwarf_weed.yaml
index c21331b..559edf3 100644
--- a/data/items/dwarf_weed.yaml
+++ b/data/items/dwarf_weed.yaml
@@ -3,4 +3,4 @@ name: "dwarf weed"
color: "48"
description: "A clean dwarf weed leaf, ready for use in pharmacy."
value: 85
-stackable: false
+stackable: true
diff --git a/data/items/dwarf_weed_seed.yaml b/data/items/dwarf_weed_seed.yaml
new file mode 100644
index 0000000..c8ba6f2
--- /dev/null
+++ b/data/items/dwarf_weed_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/guam.yaml b/data/items/guam.yaml
index 98135be..8fe7972 100644
--- a/data/items/guam.yaml
+++ b/data/items/guam.yaml
@@ -3,4 +3,4 @@ name: "guam"
color: "48"
description: "A clean guam leaf, ready for use in pharmacy."
value: 3
-stackable: false
+stackable: true
diff --git a/data/items/guam_seed.yaml b/data/items/guam_seed.yaml
new file mode 100644
index 0000000..b55e1a8
--- /dev/null
+++ b/data/items/guam_seed.yaml
@@ -0,0 +1,14 @@
+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
+farm_min_yield: 3
+farm_max_yield: 12
diff --git a/data/items/harralander.yaml b/data/items/harralander.yaml
index d8f90a4..08e66ed 100644
--- a/data/items/harralander.yaml
+++ b/data/items/harralander.yaml
@@ -3,4 +3,4 @@ name: "harralander"
color: "48"
description: "A clean harralander leaf, ready for use in pharmacy."
value: 16
-stackable: false
+stackable: true
diff --git a/data/items/harralander_seed.yaml b/data/items/harralander_seed.yaml
new file mode 100644
index 0000000..5be3571
--- /dev/null
+++ b/data/items/harralander_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/irit.yaml b/data/items/irit.yaml
index b4d4b56..40f1956 100644
--- a/data/items/irit.yaml
+++ b/data/items/irit.yaml
@@ -3,4 +3,4 @@ name: "irit"
color: "48"
description: "A clean irit leaf, ready for use in pharmacy."
value: 40
-stackable: false
+stackable: true
diff --git a/data/items/irit_seed.yaml b/data/items/irit_seed.yaml
new file mode 100644
index 0000000..4b8bad2
--- /dev/null
+++ b/data/items/irit_seed.yaml
@@ -0,0 +1,14 @@
+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
+farm_min_yield: 3
+farm_max_yield: 10
diff --git a/data/items/kwuarm.yaml b/data/items/kwuarm.yaml
index 2cd0a4e..ab0d7ff 100644
--- a/data/items/kwuarm.yaml
+++ b/data/items/kwuarm.yaml
@@ -3,4 +3,4 @@ name: "kwuarm"
color: "48"
description: "A clean kwuarm leaf, ready for use in pharmacy."
value: 54
-stackable: false
+stackable: true
diff --git a/data/items/kwuarm_seed.yaml b/data/items/kwuarm_seed.yaml
new file mode 100644
index 0000000..053c2f3
--- /dev/null
+++ b/data/items/kwuarm_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/lantadyme.yaml b/data/items/lantadyme.yaml
index 35840d8..d38b7da 100644
--- a/data/items/lantadyme.yaml
+++ b/data/items/lantadyme.yaml
@@ -3,4 +3,4 @@ name: "lantadyme"
color: "48"
description: "A clean lantadyme leaf, ready for use in pharmacy."
value: 78
-stackable: false
+stackable: true
diff --git a/data/items/lantadyme_seed.yaml b/data/items/lantadyme_seed.yaml
new file mode 100644
index 0000000..f4be89c
--- /dev/null
+++ b/data/items/lantadyme_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/marrentill.yaml b/data/items/marrentill.yaml
index 486d7fd..4bd492d 100644
--- a/data/items/marrentill.yaml
+++ b/data/items/marrentill.yaml
@@ -3,4 +3,4 @@ name: "marrentill"
color: "48"
description: "A clean marrentill leaf, ready for use in pharmacy."
value: 6
-stackable: false
+stackable: true
diff --git a/data/items/marrentill_seed.yaml b/data/items/marrentill_seed.yaml
new file mode 100644
index 0000000..4b42353
--- /dev/null
+++ b/data/items/marrentill_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/onion.yaml b/data/items/onion.yaml
new file mode 100644
index 0000000..0d0aca7
--- /dev/null
+++ b/data/items/onion.yaml
@@ -0,0 +1,8 @@
+id: onion
+name: onion
+color: "229"
+description: "A freshly harvested onion."
+value: 3
+stackable: true
+heal_value: 1
+eat_message: "You eat the onion. It's surprisingly sweet."
diff --git a/data/items/onion_seed.yaml b/data/items/onion_seed.yaml
index fbf6a8a..a1231ad 100644
--- a/data/items/onion_seed.yaml
+++ b/data/items/onion_seed.yaml
@@ -2,5 +2,13 @@ id: onion_seed
name: onion seed
color: "229"
description: "A seed for growing onions."
-value: 3
-stackable: true \ No newline at end of file
+value: 2
+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 \ No newline at end of file
diff --git a/data/items/plant_cure.yaml b/data/items/plant_cure.yaml
new file mode 100644
index 0000000..64ebe91
--- /dev/null
+++ b/data/items/plant_cure.yaml
@@ -0,0 +1,6 @@
+id: plant_cure
+name: plant cure
+color: "120"
+description: "A bio-engineered solution that cures diseased plants."
+value: 25
+stackable: true
diff --git a/data/items/potato.yaml b/data/items/potato.yaml
new file mode 100644
index 0000000..d9f521a
--- /dev/null
+++ b/data/items/potato.yaml
@@ -0,0 +1,8 @@
+id: potato
+name: potato
+color: "180"
+description: "A freshly harvested potato."
+value: 2
+stackable: true
+heal_value: 1
+eat_message: "You eat the potato."
diff --git a/data/items/potato_seed.yaml b/data/items/potato_seed.yaml
index 8cc4669..9daee8a 100644
--- a/data/items/potato_seed.yaml
+++ b/data/items/potato_seed.yaml
@@ -3,4 +3,12 @@ name: potato seed
color: "94"
description: "A seed for growing potatoes."
value: 2
-stackable: true \ No newline at end of file
+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 \ No newline at end of file
diff --git a/data/items/rake.yaml b/data/items/rake.yaml
new file mode 100644
index 0000000..5949626
--- /dev/null
+++ b/data/items/rake.yaml
@@ -0,0 +1,7 @@
+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
diff --git a/data/items/ranarr.yaml b/data/items/ranarr.yaml
index 690e2d0..683cce6 100644
--- a/data/items/ranarr.yaml
+++ b/data/items/ranarr.yaml
@@ -3,4 +3,4 @@ name: "ranarr"
color: "48"
description: "A clean ranarr leaf, ready for use in pharmacy."
value: 35
-stackable: false
+stackable: true
diff --git a/data/items/ranarr_seed.yaml b/data/items/ranarr_seed.yaml
index af464ec..ef54c0d 100644
--- a/data/items/ranarr_seed.yaml
+++ b/data/items/ranarr_seed.yaml
@@ -3,4 +3,12 @@ name: ranarr seed
color: "28"
description: "A rare herb seed with potent alchemical properties."
value: 500
-stackable: true \ No newline at end of file
+stackable: true
+farm_patch_type: herb
+farm_level: 32
+farm_plant_xp: 27
+farm_harvest_xp: 31
+farm_stages: 4
+farm_product: ranarr
+farm_min_yield: 3
+farm_max_yield: 10 \ No newline at end of file
diff --git a/data/items/snapdragon.yaml b/data/items/snapdragon.yaml
index c963313..0c3ba2f 100644
--- a/data/items/snapdragon.yaml
+++ b/data/items/snapdragon.yaml
@@ -3,4 +3,4 @@ name: "snapdragon"
color: "48"
description: "A clean snapdragon leaf, ready for use in pharmacy."
value: 65
-stackable: false
+stackable: true
diff --git a/data/items/snapdragon_seed.yaml b/data/items/snapdragon_seed.yaml
index 3ec440d..06f2a9e 100644
--- a/data/items/snapdragon_seed.yaml
+++ b/data/items/snapdragon_seed.yaml
@@ -1,6 +1,14 @@
id: snapdragon_seed
name: snapdragon seed
-color: "92"
-description: "An extremely rare herb seed. Highly valued by alchemists."
-value: 1500
-stackable: true \ No newline at end of file
+color: "161"
+description: "A bright red snapdragon seed, prized by pharma-crafters."
+value: 750
+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 \ No newline at end of file
diff --git a/data/items/spade.yaml b/data/items/spade.yaml
new file mode 100644
index 0000000..7d0b465
--- /dev/null
+++ b/data/items/spade.yaml
@@ -0,0 +1,7 @@
+id: spade
+name: spade
+color: "241"
+description: "A metal spade used for planting seeds and harvesting crops."
+value: 8
+stackable: false
+tool_type: spade
diff --git a/data/items/strawberry.yaml b/data/items/strawberry.yaml
new file mode 100644
index 0000000..337a65e
--- /dev/null
+++ b/data/items/strawberry.yaml
@@ -0,0 +1,8 @@
+id: strawberry
+name: strawberry
+color: "197"
+description: "A freshly harvested strawberry."
+value: 10
+stackable: true
+heal_value: 3
+eat_message: "You eat the strawberry."
diff --git a/data/items/strawberry_seed.yaml b/data/items/strawberry_seed.yaml
index bcf8ddc..8b2f45c 100644
--- a/data/items/strawberry_seed.yaml
+++ b/data/items/strawberry_seed.yaml
@@ -3,4 +3,12 @@ name: strawberry seed
color: "197"
description: "A seed for growing strawberries."
value: 40
-stackable: true \ No newline at end of file
+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 \ No newline at end of file
diff --git a/data/items/sweetcorn.yaml b/data/items/sweetcorn.yaml
new file mode 100644
index 0000000..32f5a70
--- /dev/null
+++ b/data/items/sweetcorn.yaml
@@ -0,0 +1,8 @@
+id: sweetcorn
+name: sweetcorn
+color: "226"
+description: "A freshly harvested ear of sweetcorn."
+value: 6
+stackable: true
+heal_value: 2
+eat_message: "You eat the sweetcorn."
diff --git a/data/items/sweetcorn_seed.yaml b/data/items/sweetcorn_seed.yaml
index 56bc587..d85b35b 100644
--- a/data/items/sweetcorn_seed.yaml
+++ b/data/items/sweetcorn_seed.yaml
@@ -3,4 +3,12 @@ name: sweetcorn seed
color: "226"
description: "A seed for growing sweetcorn."
value: 25
-stackable: true \ No newline at end of file
+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 \ No newline at end of file
diff --git a/data/items/tarromin.yaml b/data/items/tarromin.yaml
index 8f9e61c..5af2573 100644
--- a/data/items/tarromin.yaml
+++ b/data/items/tarromin.yaml
@@ -3,4 +3,4 @@ name: "tarromin"
color: "48"
description: "A clean tarromin leaf, ready for use in pharmacy."
value: 10
-stackable: false
+stackable: true
diff --git a/data/items/tarromin_seed.yaml b/data/items/tarromin_seed.yaml
new file mode 100644
index 0000000..c10483a
--- /dev/null
+++ b/data/items/tarromin_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/toadflax.yaml b/data/items/toadflax.yaml
index adcd5b7..0de8f01 100644
--- a/data/items/toadflax.yaml
+++ b/data/items/toadflax.yaml
@@ -3,4 +3,4 @@ name: "toadflax"
color: "48"
description: "A clean toadflax leaf, ready for use in pharmacy."
value: 25
-stackable: false
+stackable: true
diff --git a/data/items/toadflax_seed.yaml b/data/items/toadflax_seed.yaml
new file mode 100644
index 0000000..1ef6cb2
--- /dev/null
+++ b/data/items/toadflax_seed.yaml
@@ -0,0 +1,14 @@
+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
diff --git a/data/items/tomato.yaml b/data/items/tomato.yaml
new file mode 100644
index 0000000..d2ad349
--- /dev/null
+++ b/data/items/tomato.yaml
@@ -0,0 +1,8 @@
+id: tomato
+name: tomato
+color: "196"
+description: "A freshly harvested tomato."
+value: 4
+stackable: true
+heal_value: 2
+eat_message: "You eat the tomato."
diff --git a/data/items/tomato_seed.yaml b/data/items/tomato_seed.yaml
index 622db9b..c1db131 100644
--- a/data/items/tomato_seed.yaml
+++ b/data/items/tomato_seed.yaml
@@ -2,5 +2,13 @@ id: tomato_seed
name: tomato seed
color: "196"
description: "A seed for growing tomatoes."
-value: 8
-stackable: true \ No newline at end of file
+value: 3
+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 \ No newline at end of file
diff --git a/data/items/torstol.yaml b/data/items/torstol.yaml
index db9ed3c..a8b8fbb 100644
--- a/data/items/torstol.yaml
+++ b/data/items/torstol.yaml
@@ -3,4 +3,4 @@ name: "torstol"
color: "48"
description: "A clean torstol leaf, ready for use in pharmacy."
value: 100
-stackable: false
+stackable: true
diff --git a/data/items/torstol_seed.yaml b/data/items/torstol_seed.yaml
index 5db6f9d..2dfd60c 100644
--- a/data/items/torstol_seed.yaml
+++ b/data/items/torstol_seed.yaml
@@ -3,4 +3,12 @@ name: torstol seed
color: "46"
description: "The rarest of herb seeds. Worth a small fortune."
value: 5000
-stackable: true \ No newline at end of file
+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 \ No newline at end of file
diff --git a/data/items/watering_can.yaml b/data/items/watering_can.yaml
new file mode 100644
index 0000000..4a75de9
--- /dev/null
+++ b/data/items/watering_can.yaml
@@ -0,0 +1,7 @@
+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
diff --git a/data/items/watermelon.yaml b/data/items/watermelon.yaml
new file mode 100644
index 0000000..3374687
--- /dev/null
+++ b/data/items/watermelon.yaml
@@ -0,0 +1,8 @@
+id: watermelon
+name: watermelon
+color: "34"
+description: "A freshly harvested watermelon."
+value: 18
+stackable: true
+heal_value: 4
+eat_message: "You eat the watermelon."
diff --git a/data/items/watermelon_seed.yaml b/data/items/watermelon_seed.yaml
index adfecc2..7ea59c6 100644
--- a/data/items/watermelon_seed.yaml
+++ b/data/items/watermelon_seed.yaml
@@ -3,4 +3,12 @@ name: watermelon seed
color: "34"
description: "A seed for growing watermelons."
value: 80
-stackable: true \ No newline at end of file
+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 \ No newline at end of file
diff --git a/data/objects/allotment_patch.yaml b/data/objects/allotment_patch.yaml
new file mode 100644
index 0000000..9d905b0
--- /dev/null
+++ b/data/objects/allotment_patch.yaml
@@ -0,0 +1,7 @@
+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."
diff --git a/data/objects/flower_patch.yaml b/data/objects/flower_patch.yaml
new file mode 100644
index 0000000..c5f6d42
--- /dev/null
+++ b/data/objects/flower_patch.yaml
@@ -0,0 +1,7 @@
+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."
diff --git a/data/objects/herb_patch.yaml b/data/objects/herb_patch.yaml
new file mode 100644
index 0000000..cca6e27
--- /dev/null
+++ b/data/objects/herb_patch.yaml
@@ -0,0 +1,7 @@
+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."
diff --git a/data/objects/tool_shed.yaml b/data/objects/tool_shed.yaml
new file mode 100644
index 0000000..752c921
--- /dev/null
+++ b/data/objects/tool_shed.yaml
@@ -0,0 +1,7 @@
+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."
diff --git a/data/rooms/15.yaml b/data/rooms/15.yaml
index b51840a..a67624d 100644
--- a/data/rooms/15.yaml
+++ b/data/rooms/15.yaml
@@ -4,6 +4,7 @@ description: "Shelves lined with glass vials, dried herbs, and bubbling cauldron
exits:
east: 16
west: 14
+ south: 150
mobs:
- id: lesser_drone
- id: lesser_drone
diff --git a/data/rooms/150.yaml b/data/rooms/150.yaml
new file mode 100644
index 0000000..462a30c
--- /dev/null
+++ b/data/rooms/150.yaml
@@ -0,0 +1,32 @@
+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. Exits lead to specialized growing areas."
+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
diff --git a/data/rooms/151.yaml b/data/rooms/151.yaml
new file mode 100644
index 0000000..1bb25bf
--- /dev/null
+++ b/data/rooms/151.yaml
@@ -0,0 +1,11 @@
+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. The air here smells faintly of fresh vegetation."
+map_symbol: "G"
+exits:
+ west: 150
+objects:
+ - id: herb_patch
+ - id: herb_patch
+ - id: herb_patch
+ - id: herb_patch
diff --git a/data/rooms/152.yaml b/data/rooms/152.yaml
new file mode 100644
index 0000000..5d2f62d
--- /dev/null
+++ b/data/rooms/152.yaml
@@ -0,0 +1,11 @@
+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. A single {213}flower patch{/} sits in the corner."
+map_symbol: "A"
+exits:
+ north: 150
+objects:
+ - id: allotment_patch
+ - id: allotment_patch
+ - id: allotment_patch
+ - id: flower_patch
diff --git a/internal/config/config.go b/internal/config/config.go
index ad90103..e7e1b02 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -53,6 +53,8 @@ func DefaultColors() ColorsConfig {
"science_mod": "99",
"assassin_task": "219",
"warning": "208",
+ "farm_grow": "34",
+ "farm_disease": "196",
}
}
diff --git a/internal/game/action.go b/internal/game/action.go
index 3cc51c6..be35ae2 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -241,6 +241,16 @@ func (g *Game) AdvanceActions() {
g.advanceIdentify(sess, p)
case "steal":
g.advanceSteal(sess, p)
+ 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)
default:
if productionActionTypes[p.Action.Type] {
g.advanceProduction(sess, p)
diff --git a/internal/game/action_farm.go b/internal/game/action_farm.go
new file mode 100644
index 0000000..12b99c0
--- /dev/null
+++ b/internal/game/action_farm.go
@@ -0,0 +1,657 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "sort"
+ "strings"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+const FarmTickInterval = 500
+
+var farmPatchDefIDs = map[string]bool{
+ "herb_patch": true,
+ "allotment_patch": true,
+ "flower_patch": true,
+ "bush_patch": true,
+ "tree_patch": true,
+}
+
+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 ""
+}
+
+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
+ }
+}
+
+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
+}
+
+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)
+ }
+}
+
+func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) {
+ 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 {
+ 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
+ }
+
+ stage := intFlag(p.Flags, prefix+"_stage")
+ 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 {
+ 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 {
+ 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
+ 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)
+ }
+}
+
+func seedDisplayName(g *Game, seedID string) string {
+ if def, err := g.ItemStore.Load(seedID); err == nil {
+ return def.Name
+ }
+ return seedID
+}
+
+func (g *Game) advancePlant(sess *net.Session, p *player.Player) {
+ if p.Action == nil || p.Action.Data == nil {
+ return
+ }
+ prefix, _ := p.Action.Data["prefix"].(string)
+ seedID, _ := p.Action.Data["seed_id"].(string)
+ xp, _ := p.Action.Data["xp"].(float64)
+
+ g.ensureFarmState(p, prefix)
+
+ p.Flags[prefix+"_weeds"] = false
+ p.Flags[prefix+"_seed"] = seedID
+ 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
+
+ if intXP := int(xp); intXP > 0 {
+ if newLevel := p.AddSkillXP(player.Farming, intXP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up",
+ fmt.Sprintf("*** You are now level %d farming! ***", newLevel)))
+ }
+ }
+
+ g.AccountStore.SaveCharacter(p)
+
+ seedName := seedID
+ if def, err := g.ItemStore.Load(seedID); err == nil {
+ seedName = def.Name
+ }
+ patchName := patchDisplayName(prefix)
+ msg := fmt.Sprintf("You plant a %s in the %s.", seedName, patchName)
+ if int(xp) > 0 && p.OptionBool("xp_drops") {
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp frm)", int(xp)))
+ }
+ sess.WriteLine(msg)
+
+ p.Action = nil
+ p.ActionState = nil
+}
+
+func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) {
+ if p.Action == nil || p.Action.Data == nil {
+ return
+ }
+ prefix, _ := p.Action.Data["prefix"].(string)
+ product, _ := p.Action.Data["product"].(string)
+ minYield, _ := p.Action.Data["min_yield"].(float64)
+ maxYield, _ := p.Action.Data["max_yield"].(float64)
+ xp, _ := p.Action.Data["xp"].(float64)
+
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if !ready {
+ sess.WriteLine("There's nothing ready to harvest here.")
+ p.Action = nil
+ p.ActionState = nil
+ return
+ }
+
+ yield := int(minYield) + rand.Intn(int(maxYield)-int(minYield)+1)
+ bonus := p.Level(player.Farming) / 20
+ yield += bonus
+
+ freeSlots := p.FreeSlots()
+ if yield > freeSlots {
+ productDef, _ := g.ItemStore.Load(product)
+ if productDef == nil || !productDef.Stackable {
+ yield = freeSlots
+ }
+ }
+ if yield <= 0 {
+ sess.WriteLine("Your inventory is too full!")
+ p.Action = nil
+ p.ActionState = nil
+ return
+ }
+
+ productDef, _ := g.ItemStore.Load(product)
+ if productDef != nil && productDef.Stackable {
+ added := 0
+ for i := 0; i < 28 && added < yield; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == product {
+ slot.Quantity += yield
+ added = yield
+ break
+ }
+ }
+ if added == 0 {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot >= 0 {
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: product, Quantity: yield})
+ }
+ }
+ } else {
+ for i := 0; i < yield; i++ {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot < 0 {
+ break
+ }
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: product, Quantity: 1})
+ }
+ }
+
+ if intXP := int(xp); intXP > 0 {
+ if newLevel := p.AddSkillXP(player.Farming, intXP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up",
+ fmt.Sprintf("*** You are now level %d farming! ***", newLevel)))
+ }
+ }
+
+ g.ensureFarmState(p, prefix)
+ p.Flags[prefix+"_seed"] = ""
+ p.Flags[prefix+"_stage"] = 0
+ p.Flags[prefix+"_ready"] = false
+ p.Flags[prefix+"_watered"] = false
+ p.Flags[prefix+"_weeds"] = true
+ p.Flags[prefix+"_diseased"] = false
+ p.Flags[prefix+"_dead"] = false
+
+ g.AccountStore.SaveCharacter(p)
+
+ productName := product
+ if productDef != nil {
+ productName = productDef.Name
+ }
+ patchName := patchDisplayName(prefix)
+ msg := fmt.Sprintf("You harvest %d %s from the %s.", yield, productName, patchName)
+ if int(xp) > 0 && p.OptionBool("xp_drops") {
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp frm)", int(xp)))
+ }
+ sess.WriteLine(msg)
+
+ p.Action = nil
+ p.ActionState = nil
+}
+
+func (g *Game) advanceRake(sess *net.Session, p *player.Player) {
+ if p.Action == nil || p.Action.Data == nil {
+ return
+ }
+ prefix, _ := p.Action.Data["prefix"].(string)
+
+ p.Flags[prefix+"_weeds"] = false
+ p.Flags[prefix+"_dead"] = false
+ p.Flags[prefix+"_seed"] = ""
+ p.Flags[prefix+"_stage"] = 0
+ p.Flags[prefix+"_ready"] = false
+ p.Flags[prefix+"_diseased"] = false
+ p.Flags[prefix+"_watered"] = false
+
+ g.AccountStore.SaveCharacter(p)
+
+ patchName := patchDisplayName(prefix)
+ sess.WriteLine(fmt.Sprintf("You rake the %s clean.", patchName))
+
+ p.Action = nil
+ p.ActionState = nil
+}
+
+func (g *Game) advanceWater(sess *net.Session, p *player.Player) {
+ if p.Action == nil || p.Action.Data == nil {
+ return
+ }
+ prefix, _ := p.Action.Data["prefix"].(string)
+
+ p.Flags[prefix+"_watered"] = true
+
+ g.AccountStore.SaveCharacter(p)
+
+ patchName := patchDisplayName(prefix)
+ sess.WriteLine(fmt.Sprintf("You water the %s.", patchName))
+
+ p.Action = nil
+ p.ActionState = nil
+}
+
+func (g *Game) advanceCure(sess *net.Session, p *player.Player) {
+ if p.Action == nil || p.Action.Data == nil {
+ return
+ }
+ prefix, _ := p.Action.Data["prefix"].(string)
+
+ p.RemoveItem("plant_cure", 1)
+ p.Flags[prefix+"_diseased"] = false
+
+ g.AccountStore.SaveCharacter(p)
+
+ sess.WriteLine("You apply the plant cure. The patch looks healthy again.")
+
+ p.Action = nil
+ p.ActionState = nil
+}
+
+func patchDisplayName(prefix string) string {
+ switch {
+ case strings.Contains(prefix, "herb"):
+ return "herb patch"
+ case strings.Contains(prefix, "allot"):
+ return "allotment patch"
+ case strings.Contains(prefix, "flower"):
+ return "flower patch"
+ case strings.Contains(prefix, "bush"):
+ return "bush patch"
+ case strings.Contains(prefix, "tree"):
+ return "tree patch"
+ }
+ return "patch"
+}
+
+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 {
+ name := seedID
+ if def, err := g.ItemStore.Load(seedID); err == nil {
+ name = def.Name
+ }
+ return fmt.Sprintf("\nA fully grown %s is ready to harvest!", name)
+ }
+
+ stage := intFlag(p.Flags, prefix+"_stage")
+ maxStages := 4
+ if def, err := g.ItemStore.Load(seedID); err == nil && def.FarmStages > 0 {
+ maxStages = def.FarmStages
+ }
+ name := seedID
+ if def, err := g.ItemStore.Load(seedID); err == nil {
+ name = def.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
+}
+
+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, ", "))
+}
+
+func intFlag(flags map[string]any, key string) int {
+ v, ok := flags[key]
+ if !ok {
+ return 0
+ }
+ switch n := v.(type) {
+ case int:
+ return n
+ case float64:
+ return int(n)
+ }
+ return 0
+}
+
+func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, defID string, index int) {
+ objInstances := g.World.AllObjInstances(p.RoomID)
+ if len(objInstances) == 0 {
+ return "", "", -1
+ }
+
+ type indexedFarm struct {
+ prefix string
+ defID string
+ index int
+ name string
+ }
+ var farms []indexedFarm
+
+ for _, obj := range objInstances {
+ if !farmPatchDefIDs[obj.DefID] {
+ continue
+ }
+ for i, inst := range objInstances {
+ if inst.DefID != obj.DefID {
+ continue
+ }
+ pref := farmFlagPrefix(obj.DefID, i)
+ if pref == "" {
+ break
+ }
+ var name string
+ if def, err := g.ObjectStore.Load(obj.DefID); err == nil {
+ name = def.Name
+ } else {
+ name = obj.DefID
+ }
+ farms = append(farms, indexedFarm{prefix: pref, defID: obj.DefID, index: i, name: name})
+ break
+ }
+ break
+ }
+
+ if len(farms) == 0 {
+ return "", "", -1
+ }
+
+ if input == "" {
+ return farms[0].prefix, farms[0].defID, farms[0].index
+ }
+
+ lower := strings.ToLower(input)
+ for _, f := range farms {
+ if strings.Contains(lower, strings.ToLower(f.name)) || world.WordPrefixMatch(lower, f.name) {
+ return f.prefix, f.defID, f.index
+ }
+ }
+
+ return "", "", -1
+}
+
+func (g *Game) farmLookSuffixForObj(p *player.Player, defID string, index int) string {
+ if defID == "tool_shed" {
+ return g.toolShedLookSuffix(p)
+ }
+ if farmPatchDefIDs[defID] {
+ return g.farmPatchLookSuffix(p, defID, index)
+ }
+ return ""
+}
+
+func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) {
+ objInstances := g.World.AllObjInstances(p.RoomID)
+ hasFarmPatches := false
+ for _, obj := range objInstances {
+ if farmPatchDefIDs[obj.DefID] {
+ hasFarmPatches = true
+ break
+ }
+ }
+ if !hasFarmPatches {
+ return
+ }
+
+ type farmDisplay struct {
+ name string
+ line string
+ }
+ var displays []farmDisplay
+
+ for _, obj := range objInstances {
+ if !farmPatchDefIDs[obj.DefID] {
+ continue
+ }
+ def, _ := g.ObjectStore.Load(obj.DefID)
+ patchName := obj.DefID
+ if def != nil {
+ patchName = def.Name
+ }
+
+ prefix := farmFlagPrefix(obj.DefID, obj.Index)
+ if prefix == "" {
+ continue
+ }
+ g.ensureFarmState(p, prefix)
+
+ idxStr := ""
+ if obj.Index > 0 || g.countFarmPatchesOfType(objInstances, obj.DefID) > 1 {
+ idxStr = fmt.Sprintf(" %d", obj.Index+1)
+ }
+
+ weeds, _ := p.Flags[prefix+"_weeds"].(bool)
+ if weeds {
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("overgrown with weeds"),
+ })
+ continue
+ }
+
+ seedID, _ := p.Flags[prefix+"_seed"].(string)
+ if seedID == "" {
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("empty and ready for planting"),
+ })
+ continue
+ }
+
+ seedName := seedID
+ if sd, err := g.ItemStore.Load(seedID); err == nil {
+ seedName = sd.Name
+ }
+
+ dead, _ := p.Flags[prefix+"_dead"].(bool)
+ if dead {
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("contains a dead plant"),
+ })
+ continue
+ }
+
+ diseased, _ := p.Flags[prefix+"_diseased"].(bool)
+ if diseased {
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("has a sickly-looking %s plant", seedName),
+ })
+ continue
+ }
+
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if ready {
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("has a fully grown %s {ready to harvest}", seedName),
+ })
+ continue
+ }
+
+ stage := intFlag(p.Flags, prefix+"_stage")
+ maxStages := 4
+ if sd, err := g.ItemStore.Load(seedID); err == nil && sd.FarmStages > 0 {
+ maxStages = sd.FarmStages
+ }
+ watered, _ := p.Flags[prefix+"_watered"].(bool)
+ waterStr := ""
+ if watered {
+ waterStr = ", watered"
+ }
+ displays = append(displays, farmDisplay{
+ name: fmt.Sprintf("%s%s", patchName, idxStr),
+ line: fmt.Sprintf("has a growing %s (stage %d/%d%s)", seedName, stage, maxStages, waterStr),
+ })
+ }
+
+ if len(displays) == 0 {
+ return
+ }
+
+ sess.WriteLine("")
+ for _, d := range displays {
+ sess.WriteLine(fmt.Sprintf(" A %s: %s.", d.name, d.line))
+ }
+}
+
+func (g *Game) countFarmPatchesOfType(instances []world.ObjState, defID string) int {
+ count := 0
+ for _, obj := range instances {
+ if obj.DefID == defID && farmPatchDefIDs[obj.DefID] {
+ count++
+ }
+ }
+ return count
+}
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 2dc42ae..61288c9 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -27,6 +27,11 @@ const (
ActionIdentifying ActionType = "identifying"
ActionTriggering ActionType = "triggering"
ActionStealing ActionType = "stealing"
+ ActionPlanting ActionType = "planting"
+ ActionHarvesting ActionType = "harvesting_crop"
+ ActionRaking ActionType = "raking"
+ ActionWatering ActionType = "watering"
+ ActionCuring ActionType = "curing"
)
type ActionState struct {
@@ -88,6 +93,16 @@ func (a *ActionState) Description() string {
return "triggering " + a.TargetName
case ActionStealing:
return "stealing from " + a.TargetName
+ 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
}
return ""
}
diff --git a/internal/game/cmd_farm.go b/internal/game/cmd_farm.go
new file mode 100644
index 0000000..0a374f0
--- /dev/null
+++ b/internal/game/cmd_farm.go
@@ -0,0 +1,523 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doPlant(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if input == "" {
+ sess.WriteLine("Plant what?")
+ return
+ }
+
+ matches := g.findInventoryMatches(input, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input))
+ return
+ }
+
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ g.showWhichOne(sess, matches)
+ return
+ }
+
+ seedID := matches[0].ID
+ seedDef, err := g.ItemStore.Load(seedID)
+ if err != nil || seedDef.FarmPatchType == "" {
+ sess.WriteLine("You can't plant that.")
+ return
+ }
+
+ farmLevel := p.Level(player.Farming)
+ if seedDef.FarmLevel > 0 && farmLevel < seedDef.FarmLevel {
+ sess.WriteLine(fmt.Sprintf("You need level %d farming to plant that. Your farming level is %d.", seedDef.FarmLevel, farmLevel))
+ return
+ }
+
+ if !g.hasToolType(p, "spade") {
+ sess.WriteLine("You need a spade to plant seeds.")
+ return
+ }
+
+ patchDefID := seedDef.FarmPatchType + "_patch"
+ objInstances := g.World.FindObjInstances(p.RoomID, patchDefID)
+ if len(objInstances) == 0 {
+ sess.WriteLine("There's no suitable patch here to plant that.")
+ return
+ }
+
+ var prefix string
+ for _, obj := range objInstances {
+ pref := farmFlagPrefix(patchDefID, obj.Index)
+ if pref == "" {
+ continue
+ }
+ g.ensureFarmState(p, pref)
+ weeds, _ := p.Flags[pref+"_weeds"].(bool)
+ seedPlanted, _ := p.Flags[pref+"_seed"].(string)
+ if !weeds && seedPlanted == "" {
+ prefix = pref
+ break
+ }
+ }
+
+ if prefix == "" {
+ sess.WriteLine("All patches here have something in them. Rake them first.")
+ return
+ }
+
+ p.RemoveItem(seedID, 1)
+
+ p.Action = &action.Action{
+ Type: "plant",
+ TargetID: seedID,
+ TargetName: seedDef.Name,
+ WaitLeft: 2,
+ Data: map[string]any{
+ "seed_id": seedID,
+ "prefix": prefix,
+ "xp": float64(seedDef.FarmPlantXP),
+ },
+ }
+ p.ActionState = &ActionState{Type: ActionPlanting, TargetName: seedDef.Name}
+
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) doHarvest(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ ready, _ := flags[prefix+"_ready"].(bool)
+ return ready
+ })
+ if prefix == "" {
+ if input != "" {
+ sess.WriteLine("That patch isn't ready to harvest.")
+ } else {
+ sess.WriteLine("There's nothing ready to harvest here.")
+ }
+ return
+ }
+
+ g.ensureFarmState(p, prefix)
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if !ready {
+ sess.WriteLine("There's nothing ready to harvest here.")
+ return
+ }
+
+ if !g.hasToolType(p, "spade") {
+ sess.WriteLine("You need a spade to harvest.")
+ return
+ }
+
+ seedID, _ := p.Flags[prefix+"_seed"].(string)
+ if seedID == "" {
+ sess.WriteLine("There's nothing planted here.")
+ return
+ }
+
+ seedDef, err := g.ItemStore.Load(seedID)
+ if err != nil {
+ sess.WriteLine("Error loading seed data.")
+ return
+ }
+
+ product := seedDef.FarmProduct
+ if product == "" {
+ sess.WriteLine("This crop has no harvest product defined.")
+ return
+ }
+
+ productDef, _ := g.ItemStore.Load(product)
+ productName := product
+ if productDef != nil {
+ productName = productDef.Name
+ }
+
+ if p.FreeSlots() <= 0 && (productDef == nil || !productDef.Stackable || p.CountItem(product) == 0) {
+ sess.WriteLine("Your inventory is full.")
+ return
+ }
+
+ minYield := seedDef.FarmMinYield
+ maxYield := seedDef.FarmMaxYield
+ if minYield <= 0 {
+ minYield = 3
+ }
+ if maxYield <= 0 {
+ maxYield = 10
+ }
+ harvestXP := seedDef.FarmHarvestXP
+
+ p.Action = &action.Action{
+ Type: "harvest",
+ TargetID: prefix,
+ TargetName: productName,
+ WaitLeft: 3,
+ Data: map[string]any{
+ "prefix": prefix,
+ "seed_id": seedID,
+ "product": product,
+ "min_yield": float64(minYield),
+ "max_yield": float64(maxYield),
+ "xp": float64(harvestXP),
+ },
+ }
+ p.ActionState = &ActionState{Type: ActionHarvesting, TargetName: productName}
+
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) doRake(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if !g.hasToolType(p, "rake") {
+ sess.WriteLine("You need a rake to do that.")
+ return
+ }
+
+ prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ weeds, _ := flags[prefix+"_weeds"].(bool)
+ dead, _ := flags[prefix+"_dead"].(bool)
+ return weeds || dead
+ })
+ if prefix == "" {
+ if input != "" {
+ sess.WriteLine("That patch doesn't need raking.")
+ } else {
+ sess.WriteLine("There's nothing here that needs raking.")
+ }
+ return
+ }
+
+ g.ensureFarmState(p, prefix)
+ weeds, _ := p.Flags[prefix+"_weeds"].(bool)
+ dead, _ := p.Flags[prefix+"_dead"].(bool)
+ if !weeds && !dead {
+ sess.WriteLine("The patch doesn't need raking.")
+ return
+ }
+
+ patchName := patchDisplayName(prefix)
+
+ 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}
+
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) doWater(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if !g.hasToolType(p, "watering_can") {
+ sess.WriteLine("You need a watering can to do that.")
+ return
+ }
+
+ prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ seed, _ := flags[prefix+"_seed"].(string)
+ if seed == "" {
+ return false
+ }
+ ready, _ := flags[prefix+"_ready"].(bool)
+ dead, _ := flags[prefix+"_dead"].(bool)
+ watered, _ := flags[prefix+"_watered"].(bool)
+ return !dead && !ready && !watered
+ })
+ if prefix == "" {
+ if input != "" {
+ sess.WriteLine("That patch doesn't need watering.")
+ } else {
+ sess.WriteLine("There's nothing here that needs watering.")
+ }
+ return
+ }
+
+ g.ensureFarmState(p, prefix)
+ seedID, _ := p.Flags[prefix+"_seed"].(string)
+ if seedID == "" {
+ sess.WriteLine("There's nothing planted here to water.")
+ return
+ }
+ ready, _ := p.Flags[prefix+"_ready"].(bool)
+ if ready {
+ sess.WriteLine("The crop is already fully grown.")
+ return
+ }
+ watered, _ := p.Flags[prefix+"_watered"].(bool)
+ if watered {
+ sess.WriteLine("The patch is already watered.")
+ return
+ }
+
+ patchName := patchDisplayName(prefix)
+
+ 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}
+
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) doCure(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if !p.HasItem("plant_cure") {
+ sess.WriteLine("You don't have any plant cure.")
+ return
+ }
+
+ prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ diseased, _ := flags[prefix+"_diseased"].(bool)
+ return diseased
+ })
+ if prefix == "" {
+ if input != "" {
+ sess.WriteLine("That patch isn't diseased.")
+ } else {
+ sess.WriteLine("There's nothing here that needs curing.")
+ }
+ return
+ }
+
+ g.ensureFarmState(p, prefix)
+ diseased, _ := p.Flags[prefix+"_diseased"].(bool)
+ if !diseased {
+ sess.WriteLine("The patch isn't diseased.")
+ return
+ }
+
+ patchName := patchDisplayName(prefix)
+
+ 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}
+
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) doInspect(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ objInstances := g.World.AllObjInstances(p.RoomID)
+ var farmPatches []struct {
+ prefix string
+ defID string
+ index int
+ }
+
+ for _, obj := range objInstances {
+ if !farmPatchDefIDs[obj.DefID] {
+ continue
+ }
+ pref := farmFlagPrefix(obj.DefID, obj.Index)
+ if pref == "" {
+ continue
+ }
+ farmPatches = append(farmPatches, struct {
+ prefix string
+ defID string
+ index int
+ }{pref, obj.DefID, obj.Index})
+ }
+
+ if len(farmPatches) == 0 {
+ sess.WriteLine("There are no farming patches here.")
+ return
+ }
+
+ if input != "" {
+ lower := strings.ToLower(input)
+ var filtered []struct {
+ prefix string
+ defID string
+ index int
+ }
+ for _, fp := range farmPatches {
+ def, _ := g.ObjectStore.Load(fp.defID)
+ if def != nil && strings.Contains(strings.ToLower(def.Name), lower) {
+ filtered = append(filtered, fp)
+ }
+ }
+ if len(filtered) == 0 {
+ for _, fp := range farmPatches {
+ if strings.Contains(fp.prefix, lower) || strings.Contains(strings.ToLower(fp.defID), lower) {
+ filtered = append(filtered, fp)
+ }
+ }
+ }
+ farmPatches = filtered
+ }
+
+ for _, fp := range farmPatches {
+ g.ensureFarmState(p, fp.prefix)
+ def, _ := g.ObjectStore.Load(fp.defID)
+ patchName := fp.defID
+ if def != nil {
+ patchName = def.Name
+ }
+ indexStr := ""
+ if len(farmPatches) > 1 || fp.index > 0 {
+ indexStr = fmt.Sprintf(" %d", fp.index+1)
+ }
+
+ sess.WriteLine(fmt.Sprintf("\n=== %s%s ===", patchName, indexStr))
+
+ weeds, _ := p.Flags[fp.prefix+"_weeds"].(bool)
+ if weeds {
+ sess.WriteLine(" Status: Weeds")
+ sess.WriteLine(" (Rake to clear before planting)")
+ continue
+ }
+
+ seedID, _ := p.Flags[fp.prefix+"_seed"].(string)
+ if seedID == "" {
+ sess.WriteLine(" Status: Empty")
+ sess.WriteLine(" (Ready to plant)")
+ continue
+ }
+
+ seedName := seedID
+ if def, err := g.ItemStore.Load(seedID); err == nil {
+ seedName = def.Name
+ }
+
+ dead, _ := p.Flags[fp.prefix+"_dead"].(bool)
+ if dead {
+ sess.WriteLine(fmt.Sprintf(" Status: Dead"))
+ sess.WriteLine(fmt.Sprintf(" Planted: %s", seedName))
+ sess.WriteLine(" (Rake to clear)")
+ continue
+ }
+
+ diseased, _ := p.Flags[fp.prefix+"_diseased"].(bool)
+ if diseased {
+ sess.WriteLine(fmt.Sprintf(" Status: Diseased!"))
+ sess.WriteLine(fmt.Sprintf(" Planted: %s", seedName))
+ sess.WriteLine(" (Use plant cure to save it)")
+ continue
+ }
+
+ ready, _ := p.Flags[fp.prefix+"_ready"].(bool)
+ if ready {
+ sess.WriteLine(fmt.Sprintf(" Status: Ready to harvest!"))
+ sess.WriteLine(fmt.Sprintf(" Planted: %s", seedName))
+ continue
+ }
+
+ stage := intFlag(p.Flags, fp.prefix+"_stage")
+ maxStages := 4
+ if def, err := g.ItemStore.Load(seedID); err == nil && def.FarmStages > 0 {
+ maxStages = def.FarmStages
+ }
+ watered, _ := p.Flags[fp.prefix+"_watered"].(bool)
+
+ sess.WriteLine(fmt.Sprintf(" Status: Growing (stage %d/%d)", stage, maxStages))
+ sess.WriteLine(fmt.Sprintf(" Planted: %s", seedName))
+ sess.WriteLine(fmt.Sprintf(" Watered: %s", boolToYes(watered)))
+ sess.WriteLine(fmt.Sprintf(" Diseased: %s", boolToYes(diseased)))
+ }
+}
+
+func boolToYes(b bool) string {
+ if b {
+ return "Yes"
+ }
+ return "No"
+}
+
+func (g *Game) findFarmPatch(sess *net.Session, p *player.Player, input string, matcher func(map[string]any, string) bool) (prefix string, defID string, index int) {
+ objInstances := g.World.AllObjInstances(p.RoomID)
+ if len(objInstances) == 0 {
+ return "", "", -1
+ }
+
+ type patchInfo struct {
+ prefix string
+ defID string
+ index int
+ name string
+ }
+ var patches []patchInfo
+
+ for _, obj := range objInstances {
+ if !farmPatchDefIDs[obj.DefID] {
+ continue
+ }
+ g.ensureFarmState(p, farmFlagPrefix(obj.DefID, obj.Index))
+ }
+
+ for _, obj := range objInstances {
+ if !farmPatchDefIDs[obj.DefID] {
+ continue
+ }
+ pref := farmFlagPrefix(obj.DefID, obj.Index)
+ if pref == "" {
+ continue
+ }
+ if !matcher(p.Flags, pref) {
+ continue
+ }
+ var name string
+ if def, err := g.ObjectStore.Load(obj.DefID); err == nil {
+ name = def.Name
+ } else {
+ name = obj.DefID
+ }
+ patches = append(patches, patchInfo{prefix: pref, defID: obj.DefID, index: obj.Index, name: name})
+ }
+
+ if len(patches) == 0 {
+ return "", "", -1
+ }
+
+ if input == "" {
+ return patches[0].prefix, patches[0].defID, patches[0].index
+ }
+
+ lower := strings.ToLower(input)
+ for _, pi := range patches {
+ if strings.Contains(lower, strings.ToLower(pi.name)) {
+ return pi.prefix, pi.defID, pi.index
+ }
+ }
+ for _, pi := range patches {
+ if strings.Contains(lower, pi.prefix) {
+ return pi.prefix, pi.defID, pi.index
+ }
+ }
+
+ return "", "", -1
+}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index 14bac11..4abd89e 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -112,6 +112,10 @@ func (g *Game) doLook(sess *net.Session) {
continue
}
+ if farmPatchDefIDs[objID] {
+ continue
+ }
+
type instInfo struct {
idx int
depleted bool
@@ -212,6 +216,8 @@ func (g *Game) doLook(sess *net.Session) {
}
}
+ g.showFarmPatches(sess, p)
+
ground := g.World.GroundItemsDetailed(p.RoomID)
if len(ground) > 0 {
showDespawn := p.OptionBool("despawn")
@@ -508,6 +514,11 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
sess.WriteLine(fmt.Sprintf(" %s", desc))
}
}
+
+ farmSuffix := g.farmLookSuffixForObj(p, st.DefID, st.Index)
+ if farmSuffix != "" {
+ sess.WriteLine(farmSuffix)
+ }
return
}
diff --git a/internal/game/game.go b/internal/game/game.go
index 8b99731..e800a78 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -55,6 +55,7 @@ type Game struct {
consumeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
guardWatchTimers map[string]int
+ farmTickCounter int
}
func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
@@ -147,7 +148,7 @@ func classifyCommand(cmd string) CommandClass {
"colortable", "prompt", "style", "stats",
"tech", "t", "sneak",
"autocast", "auto", "mods", "modlist",
- "task":
+ "task", "inspect":
return ClassInstant
case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
@@ -158,7 +159,8 @@ func classifyCommand(cmd string) CommandClass {
"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix",
"id", "identify",
"steal", "thieve",
- "trigger", "cast":
+ "trigger", "cast",
+ "plant", "harvest", "rake", "water", "cure":
return ClassActive
case "eat", "fletch", "clean":
return ClassFree
@@ -461,6 +463,52 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doSteal(sess, strings.Join(args, " "))
}
return
+ 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, " "))
+ }
case "walk":
g.doWalk(sess, args)
return
@@ -517,7 +565,7 @@ func (g *Game) ProcessQueuedCommands() {
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
- ActionStealing:
+ ActionStealing, ActionPlanting, ActionHarvesting, ActionRaking, ActionWatering, ActionCuring:
default:
if as.Type != ActionMoving || p.MoveTicks <= 0 {
p.ActionState = nil
diff --git a/internal/object/item.go b/internal/object/item.go
index 6f0faac..7aed6c2 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -59,6 +59,14 @@ type ItemDef struct {
MadeFrom []MadeFromEntry `yaml:"made_from,omitempty"`
Ticks float64 `yaml:"ticks"`
ProvidesJunk string `yaml:"provides_junk,omitempty"`
+ 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"`
}
type MadeFromEntry struct {