diff options
| author | workhorse <workhorse@localhost.localdomain> | 2026-06-19 11:51:17 -0400 |
|---|---|---|
| committer | workhorse <workhorse@localhost.localdomain> | 2026-06-19 11:51:17 -0400 |
| commit | 575a71dcfed3b9fa44af244836f638a23df05a9a (patch) | |
| tree | eb5eff16eab86247a3dd93ff4d7fd3781f291b64 /internal/game | |
| parent | 52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (diff) | |
| download | thehouseoficarus-575a71dcfed3b9fa44af244836f638a23df05a9a.tar.gz | |
feat: farming roughly implemented
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/action.go | 10 | ||||
| -rw-r--r-- | internal/game/action_farm.go | 657 | ||||
| -rw-r--r-- | internal/game/action_state.go | 15 | ||||
| -rw-r--r-- | internal/game/cmd_farm.go | 523 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 11 | ||||
| -rw-r--r-- | internal/game/game.go | 54 |
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 |
