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.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.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 if p.CountItem(seedID) < 1 { sess.WriteLine("You no longer have that seed.") p.Action = nil return } p.RemoveItem(seedID, 1) 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.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 } if maxYield < minYield { maxYield = minYield } yield := int(minYield) + rand.Intn(int(maxYield)-int(minYield)+1) bonus := p.Level(player.Farming) / 20 yield += bonus productDef, _ := g.ItemStore.Load(product) stackable := productDef != nil && productDef.Stackable // The delivery target is chosen before the crop is consumed: a harvest // that cannot be stored must leave the patch ready instead of being // silently destroyed. A stackable product merges into an existing stack // and therefore needs no free slot. mergeSlot := -1 freeSlot := -1 noRoom := false if stackable { quality, maxQuality := stackProps(productDef) for i := 0; i < 28; i++ { s := p.InvSlot(i) // Only a stack with the properties a fresh harvest stack gets // may absorb it; merging into a differently qualified stack // would silently destroy one of the two qualities. if s != nil && s.ItemID == product && s.Quality == quality && s.MaxQuality == maxQuality { mergeSlot = i break } } if mergeSlot < 0 { freeSlot = p.FirstFreeSlot() } noRoom = mergeSlot < 0 && freeSlot < 0 } else { // One slot per unit: the clamp and the placement loop must count // slots the same way (a nil 0..27 slot). freeSlots := 0 for i := 0; i < 28; i++ { if p.InvSlot(i) == nil { freeSlots++ } } if yield > freeSlots { yield = freeSlots } noRoom = yield <= 0 } if noRoom { sess.WriteLine("Your inventory is too full!") p.Action = nil return } placed := yield if stackable { if mergeSlot >= 0 { p.InvSlot(mergeSlot).Quantity += yield } else { p.SetInvSlot(freeSlot, g.newInventorySlot(product, yield)) } } else { placed = 0 for placed < yield { fs := p.FirstFreeSlot() if fs < 0 { break } p.SetInvSlot(fs, g.newInventorySlot(product, 1)) placed++ } } 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.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.", placed, 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.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.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.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) 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: "overgrown with weeds", }) continue } seedID, _ := p.Flags[prefix+"_seed"].(string) if seedID == "" { displays = append(displays, farmDisplay{ name: fmt.Sprintf("%s%s", patchName, idxStr), line: "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: "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 }