From 575a71dcfed3b9fa44af244836f638a23df05a9a Mon Sep 17 00:00:00 2001 From: workhorse Date: Fri, 19 Jun 2026 11:51:17 -0400 Subject: feat: farming roughly implemented --- TODO.md | 2 +- cmd/mud/main.go | 1 + data/behaviors/tool_shed_talk.yaml | 86 +++++ data/help/cure.yaml | 15 + data/help/farming.yaml | 31 ++ data/help/harvest.yaml | 18 + data/help/inspect.yaml | 17 + data/help/plant.yaml | 16 + data/help/rake.yaml | 17 + data/help/water.yaml | 16 + data/items/avantoe.yaml | 2 +- data/items/avantoe_seed.yaml | 14 + data/items/cabbage.yaml | 8 + data/items/cabbage_seed.yaml | 14 +- data/items/cadantine.yaml | 2 +- data/items/cadantine_seed.yaml | 14 + data/items/dwarf_weed.yaml | 2 +- data/items/dwarf_weed_seed.yaml | 14 + data/items/guam.yaml | 2 +- data/items/guam_seed.yaml | 14 + data/items/harralander.yaml | 2 +- data/items/harralander_seed.yaml | 14 + data/items/irit.yaml | 2 +- data/items/irit_seed.yaml | 14 + data/items/kwuarm.yaml | 2 +- data/items/kwuarm_seed.yaml | 14 + data/items/lantadyme.yaml | 2 +- data/items/lantadyme_seed.yaml | 14 + data/items/marrentill.yaml | 2 +- data/items/marrentill_seed.yaml | 14 + data/items/onion.yaml | 8 + data/items/onion_seed.yaml | 12 +- data/items/plant_cure.yaml | 6 + data/items/potato.yaml | 8 + data/items/potato_seed.yaml | 10 +- data/items/rake.yaml | 7 + data/items/ranarr.yaml | 2 +- data/items/ranarr_seed.yaml | 10 +- data/items/snapdragon.yaml | 2 +- data/items/snapdragon_seed.yaml | 16 +- data/items/spade.yaml | 7 + data/items/strawberry.yaml | 8 + data/items/strawberry_seed.yaml | 10 +- data/items/sweetcorn.yaml | 8 + data/items/sweetcorn_seed.yaml | 10 +- data/items/tarromin.yaml | 2 +- data/items/tarromin_seed.yaml | 14 + data/items/toadflax.yaml | 2 +- data/items/toadflax_seed.yaml | 14 + data/items/tomato.yaml | 8 + data/items/tomato_seed.yaml | 12 +- data/items/torstol.yaml | 2 +- data/items/torstol_seed.yaml | 10 +- data/items/watering_can.yaml | 7 + data/items/watermelon.yaml | 8 + data/items/watermelon_seed.yaml | 10 +- data/objects/allotment_patch.yaml | 7 + data/objects/flower_patch.yaml | 7 + data/objects/herb_patch.yaml | 7 + data/objects/tool_shed.yaml | 7 + data/rooms/15.yaml | 1 + data/rooms/150.yaml | 32 ++ data/rooms/151.yaml | 11 + data/rooms/152.yaml | 11 + internal/config/config.go | 2 + internal/game/action.go | 10 + internal/game/action_farm.go | 657 +++++++++++++++++++++++++++++++++++++ internal/game/action_state.go | 15 + internal/game/cmd_farm.go | 523 +++++++++++++++++++++++++++++ internal/game/cmd_look.go | 11 + internal/game/game.go | 54 ++- internal/object/item.go | 8 + 72 files changed, 1926 insertions(+), 35 deletions(-) create mode 100644 data/behaviors/tool_shed_talk.yaml create mode 100644 data/help/cure.yaml create mode 100644 data/help/farming.yaml create mode 100644 data/help/harvest.yaml create mode 100644 data/help/inspect.yaml create mode 100644 data/help/plant.yaml create mode 100644 data/help/rake.yaml create mode 100644 data/help/water.yaml create mode 100644 data/items/avantoe_seed.yaml create mode 100644 data/items/cabbage.yaml create mode 100644 data/items/cadantine_seed.yaml create mode 100644 data/items/dwarf_weed_seed.yaml create mode 100644 data/items/guam_seed.yaml create mode 100644 data/items/harralander_seed.yaml create mode 100644 data/items/irit_seed.yaml create mode 100644 data/items/kwuarm_seed.yaml create mode 100644 data/items/lantadyme_seed.yaml create mode 100644 data/items/marrentill_seed.yaml create mode 100644 data/items/onion.yaml create mode 100644 data/items/plant_cure.yaml create mode 100644 data/items/potato.yaml create mode 100644 data/items/rake.yaml create mode 100644 data/items/spade.yaml create mode 100644 data/items/strawberry.yaml create mode 100644 data/items/sweetcorn.yaml create mode 100644 data/items/tarromin_seed.yaml create mode 100644 data/items/toadflax_seed.yaml create mode 100644 data/items/tomato.yaml create mode 100644 data/items/watering_can.yaml create mode 100644 data/items/watermelon.yaml create mode 100644 data/objects/allotment_patch.yaml create mode 100644 data/objects/flower_patch.yaml create mode 100644 data/objects/herb_patch.yaml create mode 100644 data/objects/tool_shed.yaml create mode 100644 data/rooms/150.yaml create mode 100644 data/rooms/151.yaml create mode 100644 data/rooms/152.yaml create mode 100644 internal/game/action_farm.go create mode 100644 internal/game/cmd_farm.go 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 + + 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 { -- cgit v1.2.3