aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorworkhorse <workhorse@localhost.localdomain>2026-06-19 11:51:17 -0400
committerworkhorse <workhorse@localhost.localdomain>2026-06-19 11:51:17 -0400
commit575a71dcfed3b9fa44af244836f638a23df05a9a (patch)
treeeb5eff16eab86247a3dd93ff4d7fd3781f291b64 /internal/game
parent52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (diff)
downloadthehouseoficarus-575a71dcfed3b9fa44af244836f638a23df05a9a.tar.gz
feat: farming roughly implemented
Diffstat (limited to 'internal/game')
-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
6 files changed, 1267 insertions, 3 deletions
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