package game import ( "fmt" "math/rand" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) const birdsNestItemID = "birds_nest" func loadGatherConfig(g *Game, objDefID string) *behavior.GatherConfig { def, err := g.ObjectStore.Load(objDefID) if err != nil { return nil } return def.Gather } func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) { cfg := obj.Gather if cfg == nil { sess.WriteLine("Something is wrong with this object.") return } if st.Depleted { if cfg.DepletedMessage != "" { msg := cfg.DepletedMessage if p.OptionBool("depletion") { msg += fmt.Sprintf(" (%d ticks to respawn)", int(st.DepleteTimer)) } sess.WriteLine(msg) } else if cfg.DepleteTimer > 0 { sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, int(st.DepleteTimer))) } else { sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, int(st.DepleteTimer))) } return } var wait float64 var toolName, toolType string if len(cfg.Tools) > 0 { toolSpeed, name, tType, found := g.findTool(p, cfg.Tools) if !found { names := make([]string, len(cfg.Tools)) for i, t := range cfg.Tools { names[i] = strings.ReplaceAll(t, "_", " ") } toolList := strings.Join(names, " or ") sess.WriteLine(fmt.Sprintf("You need a %s to do that.", toolList)) return } toolName = name toolType = tType wait = toolSpeed if wait < 1 { wait = 1 } } else { wait = cfg.NoToolSpeed if wait <= 0 { wait = 3 } } skillLevel := p.Level(player.SkillName(cfg.Skill)) toolEligible := filterDropsByTool(cfg.Drops, toolType, len(cfg.Tools)) eligible := filterDropsByLevel(toolEligible, skillLevel) if len(eligible) == 0 { if len(toolEligible) == 0 { if len(cfg.Tools) > 0 { sess.WriteLine("Your tool can't yield anything from this object.") } else { sess.WriteLine("Your skill can't yield anything from this object.") } } else { minLevel := 0 for _, d := range toolEligible { if d.Level > 0 && (minLevel == 0 || d.Level < minLevel) { minLevel = d.Level } } if minLevel > 0 { sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", minLevel, cfg.Skill)) } else { sess.WriteLine("You are not skilled enough to do that.") } } return } if cfg.Bait != "" { if !p.HasItem(cfg.Bait) { baitName := cfg.Bait if def, err := g.ItemStore.Load(cfg.Bait); err == nil { baitName = def.Name } sess.WriteLine(fmt.Sprintf("You need %s to fish here.", baitName)) return } } if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") return } verb := cfg.Skill switch cfg.Skill { case "woodcutting": verb = "chopping" } if cfg.BroadcastMessage != "" { bcMsg := strings.ReplaceAll(cfg.BroadcastMessage, "%o", obj.Name) g.broadcastAction(sess, bcMsg, p.Name) } else { g.broadcastAction(sess, "%s starts %s the %s.", p.Name, verb, obj.Name) } d := &behavior.GatherData{ ObjDefID: obj.ID, InstanceKey: g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), InstanceIdx: st.Index + 1, Wait: wait, Verb: verb, ToolName: toolName, ToolType: toolType, } if cfg.DepleteTimer > 0 { d.DepleteTimer = true } p.Action = &behavior.Action{ Type: behavior.TypeGather, TargetID: st.DefID, TargetName: obj.Name, WaitLeft: 0, Data: d, } } func (g *Game) advanceGather(sess *net.Session, p *player.Player) { d := p.Action.Data.(*behavior.GatherData) cfg := loadGatherConfig(g, d.ObjDefID) if cfg == nil { g.cancelAction(p) return } step := d.Step wait := d.Wait instanceKey := d.InstanceKey shared := d.DepleteTimer if step == 0 { if p.FirstFreeSlot() == -1 { sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) g.cancelAction(p) return } if cfg.Bait != "" { if !p.HasItem(cfg.Bait) { baitName := cfg.Bait if def, err := g.ItemStore.Load(cfg.Bait); err == nil { baitName = def.Name } sess.WriteLine(g.colorize(sess, "error", fmt.Sprintf("You've run out of %s.", baitName))) g.cancelAction(p) return } } sess.WriteLine(fmt.Sprintf("%s", cfg.GatherMessage)) d.Step = 1 p.Action.WaitLeft = engine.ToTicks(wait) return } if step == 2 { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.Depleted { p.Action.WaitLeft = engine.ToTicks(3) return } d.Step = 0 p.Action.WaitLeft = engine.ToTicks(wait) return } if cfg.Bait != "" { if !p.HasItem(cfg.Bait) { baitName := cfg.Bait if def, err := g.ItemStore.Load(cfg.Bait); err == nil { baitName = def.Name } sess.WriteLine(g.colorize(sess, "error", fmt.Sprintf("You've run out of %s.", baitName))) g.cancelAction(p) return } p.RemoveItem(cfg.Bait, 1) } skillLevel := p.Level(player.SkillName(cfg.Skill)) chance := behavior.SuccessChance(cfg.Success, skillLevel, 0) if rand.Float64() < chance { eligible := filterDropsByLevel(cfg.Drops, skillLevel) eligible = filterDropsByTool(eligible, d.ToolType, len(cfg.Tools)) drop := behavior.ResolveDrop(g.DataDir, eligible) if drop != nil { freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) g.cancelAction(p) return } qty := drop.Quantity if qty <= 0 { qty = 1 } itemName := drop.ItemID itemDef, _ := g.ItemStore.Load(drop.ItemID) if itemDef != nil { itemName = itemDef.Name } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) xp := drop.XP if xp > 0 { g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), xp) } g.AccountStore.SaveCharacter(p) msg := drop.SuccessMessage coloredName := g.itemColorize(sess, itemDef, itemName) if msg == "" { msg = fmt.Sprintf("You manage to get some %s.", coloredName) } else { msg = strings.ReplaceAll(msg, "%n", coloredName) msg = color.ExpandTags(g.colorMode(sess), msg) } if xp > 0 && p.OptionBool("xp_drops") { msg += g.formatXpDropSingle(sess, p, player.SkillName(cfg.Skill), xp) } sess.WriteLine(msg) g.checkBirdNest(sess, p, cfg, freeSlot) if shared { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.SharedTimer <= 0 { g.pendingDepletions = append(g.pendingDepletions, pendingDepletion{ instanceKey: instanceKey, objDefID: d.ObjDefID, targetName: p.Action.TargetName, playerNames: []string{p.Name}, }) d.Step = 2 p.Action.WaitLeft = engine.ToTicks(1) return } } if !shared && drop.Depletes { delay := engine.ToTicks(cfg.RespawnTimer) if delay <= 0 { delay = 10 } st := g.World.GetObjStateByKey(instanceKey) if st != nil { st.Depleted = true st.DepleteTimer = float64(delay) } depletionMsg := cfg.ExhaustedMessage if depletionMsg == "" { depletionMsg = fmt.Sprintf("The %s was depleted by %s!", p.Action.TargetName, p.Name) } for _, other := range g.Hub.PlayersInRoom(p.RoomID) { op := other.Player if op == nil || op.Action == nil || op.Action.Type != behavior.TypeGather { continue } if gd, ok := op.Action.Data.(*behavior.GatherData); ok && gd.InstanceKey == instanceKey { other.WriteLine(depletionMsg) if other != sess { g.cancelAction(op) g.writePrompt(other) } } } d.Step = 2 p.Action.WaitLeft = engine.ToTicks(1) return } if p.FirstFreeSlot() == -1 { sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) g.cancelAction(p) return } d.Step = 0 p.Action.WaitLeft = engine.ToTicks(wait) return } } sess.WriteLine(cfg.FailMessage) if p.FirstFreeSlot() == -1 { sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) g.cancelAction(p) return } d.Step = 0 p.Action.WaitLeft = engine.ToTicks(wait) } func filterDropsByLevel(drops []behavior.DropEntry, level int) []behavior.DropEntry { var eligible []behavior.DropEntry for _, d := range drops { if level >= d.Level { eligible = append(eligible, d) } } return eligible } func filterDropsByTool(drops []behavior.DropEntry, activeToolType string, toolCount int) []behavior.DropEntry { if toolCount <= 1 { out := make([]behavior.DropEntry, 0, len(drops)) for _, d := range drops { if d.Tool == "" || d.Tool == activeToolType { out = append(out, d) } } return out } out := make([]behavior.DropEntry, 0, len(drops)) for _, d := range drops { if d.Tool == activeToolType { out = append(out, d) } } return out } func (g *Game) depleteSharedTree(st *world.ObjState, cfg *behavior.GatherConfig, targetName string, playerNames []string) { st.Depleted = true st.DepleteTimer = float64(engine.ToTicks(cfg.RespawnTimer)) st.SharedTimer = 0 msg := cfg.ExhaustedMessage if msg == "" { msg = fmt.Sprintf("The %s falls to the ground!", targetName) } for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { op := sess.Player if op == nil || op.Action == nil || op.Action.Type != behavior.TypeGather { continue } if gd, ok := op.Action.Data.(*behavior.GatherData); !ok || gd.InstanceKey != g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) { continue } isDepleter := false for _, name := range playerNames { if op.Name == name { isDepleter = true break } } if isDepleter { sess.WriteLine(msg) continue } sess.WriteLine(msg) g.cancelAction(op) g.writePrompt(sess) } } func (g *Game) flushPendingDepletions() { if len(g.pendingDepletions) == 0 { return } merged := make(map[string]*pendingDepletion) for _, pd := range g.pendingDepletions { if existing, ok := merged[pd.instanceKey]; ok { existing.playerNames = append(existing.playerNames, pd.playerNames...) } else { copy := pd merged[pd.instanceKey] = © } } for _, pd := range merged { st := g.World.GetObjStateByKey(pd.instanceKey) if st == nil || st.Depleted { continue } cfg := loadGatherConfig(g, pd.objDefID) if cfg == nil { continue } g.depleteSharedTree(st, cfg, pd.targetName, pd.playerNames) } g.pendingDepletions = nil } func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *behavior.GatherConfig, currentDropSlot int) { if cfg.NestChance <= 0 { return } if rand.Intn(cfg.NestChance) != 0 { return } nestName := "bird's nest" nestDef, _ := g.ItemStore.Load(birdsNestItemID) if nestDef != nil { nestName = g.itemColorize(sess, nestDef, nestDef.Name) } freeSlot := -1 for i := 0; i < 28; i++ { if i == currentDropSlot { continue } if p.InvSlot(i) == nil { freeSlot = i break } } if freeSlot == -1 { g.World.AddGroundItem(p.RoomID, birdsNestItemID, 1) sess.WriteLine(fmt.Sprintf("A %s falls to the ground.", nestName)) return } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: birdsNestItemID, Quantity: 1}) g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("A %s falls out of the tree!", nestName)) }