package game import ( "fmt" "math/rand" "sort" "strings" "thehouseoficarus/internal/behavior" "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) { p.EnsureFlags() if _, exists := p.Flags[prefix+"_seed"]; !exists { g.setPlayerFlag(p, prefix+"_weeds", true) g.setPlayerFlag(p, prefix+"_seed", "") g.setPlayerFlag(p, prefix+"_stage", 0) g.setPlayerFlag(p, prefix+"_watered", false) g.setPlayerFlag(p, prefix+"_diseased", false) g.setPlayerFlag(p, prefix+"_dead", false) g.setPlayerFlag(p, prefix+"_ready", false) } } func (g *Game) findFarmPrefixes(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 := sess.Player if p == nil || p.Flags == nil { continue } g.advanceFarmGrowth(sess, p) } } func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) { prefixes := g.findFarmPrefixes(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 { g.setPlayerFlag(p, prefix+"_dead", true) g.setPlayerFlag(p, prefix+"_diseased", false) sess.WriteLine(g.colorize(sess, "farm_disease", fmt.Sprintf("Your %s has died from disease!", g.seedDisplayName(sess, seedID)))) g.AccountStore.SaveCharacter(p) continue } stage := intFromFlag(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 { g.setPlayerFlag(p, prefix+"_stage", stage) g.setPlayerFlag(p, prefix+"_ready", true) g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_grow", fmt.Sprintf("Your %s is fully grown and ready to harvest!", g.seedDisplayName(sess, seedID)))) } else { if !watered && rand.Float64() < 0.10 { g.setPlayerFlag(p, prefix+"_stage", stage) g.setPlayerFlag(p, prefix+"_diseased", true) g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_disease", fmt.Sprintf("Your %s has become diseased!", g.seedDisplayName(sess, seedID)))) } else { g.setPlayerFlag(p, prefix+"_stage", stage) g.setPlayerFlag(p, prefix+"_watered", false) sess.WriteLine(g.colorize(sess, "farm_grow", fmt.Sprintf("Your %s has grown to stage %d/%d.", g.seedDisplayName(sess, seedID), stage, maxStages))) } } g.AccountStore.SaveCharacter(p) } } func (g *Game) seedDisplayName(sess *net.Session, seedID string) string { if def, err := g.ItemStore.Load(seedID); err == nil { return g.itemColorize(sess, def, def.Name) } return seedID } func (g *Game) advancePlant(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } prefix := data.Prefix seedID := data.SeedID xp := data.XP g.ensureFarmState(p, prefix) g.setPlayerFlag(p, prefix+"_weeds", false) g.setPlayerFlag(p, prefix+"_seed", seedID) g.setPlayerFlag(p, prefix+"_stage", 0) g.setPlayerFlag(p, prefix+"_watered", false) g.setPlayerFlag(p, prefix+"_diseased", false) g.setPlayerFlag(p, prefix+"_dead", false) g.setPlayerFlag(p, prefix+"_ready", false) if intXP := int(xp); intXP > 0 { g.awardSkillXP(sess, p, player.Farming, intXP) } g.AccountStore.SaveCharacter(p) seedName := seedID if def, err := g.ItemStore.Load(seedID); err == nil { seedName = g.itemColorize(sess, def, 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.formatXpDropSingle(sess, p, player.Farming, int(xp)) } sess.WriteLine(msg) p.Action = nil } func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } prefix := data.Prefix product := data.Product minYield := data.MinYield maxYield := data.MaxYield xp := data.XP ready, _ := p.Flags[prefix+"_ready"].(bool) if !ready { sess.WriteLine("There's nothing ready to harvest here.") p.Action = 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 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 { g.awardSkillXP(sess, p, player.Farming, intXP) } g.ensureFarmState(p, prefix) g.setPlayerFlag(p, prefix+"_seed", "") g.setPlayerFlag(p, prefix+"_stage", 0) g.setPlayerFlag(p, prefix+"_ready", false) g.setPlayerFlag(p, prefix+"_watered", false) g.setPlayerFlag(p, prefix+"_weeds", true) g.setPlayerFlag(p, prefix+"_diseased", false) g.setPlayerFlag(p, prefix+"_dead", false) g.AccountStore.SaveCharacter(p) productName := product if productDef != nil { productName = g.itemColorize(sess, productDef, 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.formatXpDropSingle(sess, p, player.Farming, int(xp)) } sess.WriteLine(msg) p.Action = nil } func (g *Game) advanceRake(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } prefix := data.Prefix g.setPlayerFlag(p, prefix+"_weeds", false) g.setPlayerFlag(p, prefix+"_dead", false) g.setPlayerFlag(p, prefix+"_seed", "") g.setPlayerFlag(p, prefix+"_stage", 0) g.setPlayerFlag(p, prefix+"_ready", false) g.setPlayerFlag(p, prefix+"_diseased", false) g.setPlayerFlag(p, prefix+"_watered", false) g.AccountStore.SaveCharacter(p) patchName := patchDisplayName(prefix) sess.WriteLine(fmt.Sprintf("You rake the %s clean.", patchName)) p.Action = nil } func (g *Game) advanceWater(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } prefix := data.Prefix g.setPlayerFlag(p, prefix+"_watered", true) g.AccountStore.SaveCharacter(p) patchName := patchDisplayName(prefix) sess.WriteLine(fmt.Sprintf("You water the %s.", patchName)) p.Action = nil } func (g *Game) advanceCure(sess *net.Session, p *player.Player) { if p.Action == nil || p.Action.Data == nil { return } data, ok := p.Action.Data.(*behavior.FarmData) if !ok { return } prefix := data.Prefix p.RemoveItem("plant_cure", 1) g.setPlayerFlag(p, prefix+"_diseased", false) g.AccountStore.SaveCharacter(p) sess.WriteLine("You apply the plant cure. The patch looks healthy again.") p.Action = 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 "It is overgrown with weeds." } seedID, _ := p.Flags[prefix+"_seed"].(string) if seedID == "" { return "The patch is empty and ready for planting." } dead, _ := p.Flags[prefix+"_dead"].(bool) if dead { return "The plant has died. You need to rake it clean." } diseased, _ := p.Flags[prefix+"_diseased"].(bool) if diseased { return "The 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("A fully grown %s is ready to harvest!", name) } stage := intFromFlag(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("A %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 "The shed is empty." } return fmt.Sprintf("Inside: %s.", strings.Join(stored, ", ")) } 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 } pref := farmFlagPrefix(obj.DefID, obj.Index) if pref == "" { continue } 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: obj.Index, name: name}) } 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)) || behavior.WordPrefixMatch(lower, f.name) { return f.prefix, f.defID, f.index } } return "", "", -1 } func (g *Game) farmObjSuffix(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) []string { objInstances := g.World.AllObjInstances(p.RoomID) hasFarmPatches := false for _, obj := range objInstances { if farmPatchDefIDs[obj.DefID] { hasFarmPatches = true break } } if !hasFarmPatches { return nil } 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.countPatchesByType(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 = g.itemColorize(sess, sd, 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 := intFromFlag(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 nil } var lines []string lines = append(lines, "") for _, d := range displays { lines = append(lines, fmt.Sprintf("A %s: %s.", d.name, d.line)) } return lines } func (g *Game) countPatchesByType(instances []world.ObjState, defID string) int { count := 0 for _, obj := range instances { if obj.DefID == defID && farmPatchDefIDs[obj.DefID] { count++ } } return count }