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/action_farm.go | |
| parent | 52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (diff) | |
| download | thehouseoficarus-575a71dcfed3b9fa44af244836f638a23df05a9a.tar.gz | |
feat: farming roughly implemented
Diffstat (limited to 'internal/game/action_farm.go')
| -rw-r--r-- | internal/game/action_farm.go | 657 |
1 files changed, 657 insertions, 0 deletions
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 +} |
