package game import ( "fmt" "math/rand" "strings" "thirdcollapse/internal/action" "thirdcollapse/internal/net" "thirdcollapse/internal/object" "thirdcollapse/internal/player" "thirdcollapse/internal/world" ) func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) { cfg, err := g.BehaviorStore.LoadGather(obj.BehaviorID) if err != nil { sess.WriteLine("Something is wrong with this object.") return } skillLevel := p.Level(player.SkillName(cfg.Skill)) if cfg.Level > 0 && skillLevel < cfg.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", cfg.Level, cfg.Skill)) return } if st.Depleted { if cfg.DepletedMessage != "" { msg := cfg.DepletedMessage if p.OptionBool("depletion") { msg += fmt.Sprintf(" (%d ticks to respawn)", st.DepleteTimer) } sess.WriteLine(msg) } else if cfg.SharedDeplete > 0 { sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, st.DepleteTimer)) } else { sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer)) } return } wait := cfg.BaseWait if len(cfg.Tools) > 0 { toolSpeed := -1 if itemID, ok := p.Equipment[object.SlotMainHand]; ok { def, err := g.ItemStore.Load(itemID) if err == nil { for _, t := range cfg.Tools { if def.ToolType == t { toolSpeed = def.ToolSpeed break } } } } if toolSpeed < 0 { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil { continue } for _, t := range cfg.Tools { if def.ToolType == t { toolSpeed = def.ToolSpeed break } } if toolSpeed >= 0 { break } } } if toolSpeed < 0 { 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 } wait = cfg.BaseWait - toolSpeed if wait < 1 { wait = 1 } } 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 } data := map[string]any{ "behavior_id": obj.BehaviorID, "instance_key": g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), "instance_idx": st.Index + 1, "effective_wait": wait, "step": 0, } if cfg.SharedDeplete > 0 { data["shared_deplete"] = true } p.Action = &action.Action{ Type: "gather", TargetID: st.DefID, TargetName: obj.Name, WaitLeft: 0, Data: data, } } func (g *Game) advanceGather(sess *net.Session, p *player.Player) { behaviorID := p.Action.Data["behavior_id"].(string) cfg, err := g.BehaviorStore.LoadGather(behaviorID) if err != nil { g.CancelAction(p) return } step := p.Action.Data["step"].(int) wait := p.Action.Data["effective_wait"].(int) instanceKey := p.Action.Data["instance_key"].(string) _, shared := p.Action.Data["shared_deplete"] if step == 0 { 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've run out of %s.", baitName)) g.CancelAction(p) return } } sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg)) p.Action.Data["step"] = 1 p.Action.WaitLeft = wait return } if step == 2 { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.Depleted { p.Action.WaitLeft = 3 return } if cfg.RespawnMsg != "" { sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg)) } p.Action.Data["step"] = 0 p.Action.WaitLeft = 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(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 := action.SuccessChance(cfg.Success, skillLevel, cfg.Level) if rand.Float64() < chance { eligible := filterDropsByLevel(cfg.Drops, skillLevel) drop := g.BehaviorStore.ResolveDrop(eligible) if drop != nil { freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } qty := drop.Quantity if qty <= 0 { qty = 1 } itemName := drop.ItemID if def, err := g.ItemStore.Load(drop.ItemID); err == nil { itemName = def.Name } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) xp := drop.XP if xp <= 0 { xp = cfg.XP } if xp > 0 { if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), xp); newLevel > 0 { sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)) } } g.AccountStore.SaveCharacter(p) msg := drop.Message if msg == "" { msg = fmt.Sprintf("You manage to get some %s.", itemName) } if xp > 0 && p.OptionBool("xpdrops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)]) } sess.WriteLine(msg) g.checkBirdNest(sess, p, cfg, freeSlot) if shared { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.SharedTimer <= 0 { g.depleteSharedTree(sess, p, st, cfg, instanceKey) return } } if !shared && drop.Depletes { delay := cfg.DepleteDelay if delay <= 0 { delay = 10 } st := g.World.GetObjStateByKey(instanceKey) if st != nil { st.Depleted = true st.DepleteTimer = delay } for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other == sess { continue } op, ok := other.Player.(*player.Player) if !ok || op.Action == nil || op.Action.Type != "gather" { continue } if op.Action.Data["instance_key"] == instanceKey { other.WriteLine(fmt.Sprintf("\nThe %s was depleted by %s!", p.Action.TargetName, p.Name)) g.CancelAction(op) } } p.Action.Data["step"] = 2 p.Action.WaitLeft = 1 return } if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } p.Action.Data["step"] = 0 p.Action.WaitLeft = wait return } } sess.WriteLine(cfg.FailMsg) if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } p.Action.Data["step"] = 0 p.Action.WaitLeft = wait } func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry { var eligible []action.DropEntry for _, d := range drops { if level >= d.Level { eligible = append(eligible, d) } } if len(eligible) == 0 { return drops } return eligible } func (g *Game) depleteSharedTree(sess *net.Session, p *player.Player, st *world.ObjState, cfg *action.GatherConfig, instanceKey string) { st.Depleted = true st.DepleteTimer = cfg.DepleteDelay st.SharedTimer = 0 msg := cfg.ExhaustedMessage if msg == "" { msg = fmt.Sprintf("The %s falls to the ground!", p.Action.TargetName) } sess.WriteLine(fmt.Sprintf("\n%s", msg)) for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other == sess { continue } op, ok := other.Player.(*player.Player) if !ok || op.Action == nil || op.Action.Type != "gather" { continue } if op.Action.Data["instance_key"] == instanceKey { other.WriteLine(fmt.Sprintf("\n%s", msg)) g.CancelAction(op) } } p.Action.Data["step"] = 2 p.Action.WaitLeft = 1 } func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *action.GatherConfig, currentDropSlot int) { if cfg.NestChance <= 0 { return } if rand.Intn(cfg.NestChance) != 0 { return } nestName := "bird's nest" if def, err := g.ItemStore.Load("birds_nest"); err == nil { nestName = def.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, "birds_nest", 1) sess.WriteLine(fmt.Sprintf("A %s falls to the ground.", nestName)) return } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: "birds_nest", Quantity: 1}) g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("A %s falls out of the tree!", nestName)) }