package game import ( "fmt" "math/rand" "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/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)", 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 } wait := cfg.BaseWait if len(cfg.Tools) > 0 { var toolSpeed float64 = -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 } toolName := "" if itemID, ok := p.Equipment[object.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil { for _, t := range cfg.Tools { if def.ToolType == t { toolName = def.Name break } } } } if toolName == "" { for _, slot := range p.Inventory { if slot == nil || slot.Quantity <= 0 { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil { continue } for _, t := range cfg.Tools { if def.ToolType == t { toolName = def.Name break } } if toolName != "" { break } } } verb := cfg.Skill switch cfg.Skill { case "woodcutting": verb = "chopping" } p.ActionState = &ActionState{Type: ActionGathering, TargetName: obj.Name, ToolName: toolName, Verb: verb} 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.DepleteTimer > 0 { data["deplete_timer"] = 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"].(float64) instanceKey := p.Action.Data["instance_key"].(string) _, shared := p.Action.Data["deplete_timer"] 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("\n%s", cfg.GatherMsg)) p.Action.Data["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 } p.Action.Data["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 := 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(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 { xp = cfg.XP } if xp > 0 { if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), xp); newLevel > 0 { sess.WriteLine(g.colorize(sess, "level_up", 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.", g.itemColorize(sess, itemDef, itemName)) } else { msg = color.ExpandTags(g.colorMode(sess), msg) } if xp > 0 && p.OptionBool("xp_drops") { msg += g.colorize(sess, "xp", 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.pendingDepletions = append(g.pendingDepletions, pendingDepletion{ instanceKey: instanceKey, behaviorID: behaviorID, targetName: p.Action.TargetName, playerNames: []string{p.Name}, }) p.Action.Data["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) } 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!", g.colorize(sess, "item", p.Action.TargetName), g.colorize(sess, "player_name", p.Name))) g.CancelAction(op) g.writePrompt(other) } } p.Action.Data["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 } p.Action.Data["step"] = 0 p.Action.WaitLeft = engine.ToTicks(wait) return } } sess.WriteLine(cfg.FailMsg) if p.FirstFreeSlot() == -1 { sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) g.CancelAction(p) return } p.Action.Data["step"] = 0 p.Action.WaitLeft = engine.ToTicks(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(st *world.ObjState, cfg *action.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, ok := sess.Player.(*player.Player) if !ok || op.Action == nil || op.Action.Type != "gather" { continue } if op.Action.Data["instance_key"] != 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(fmt.Sprintf("\n%s", msg)) continue } sess.WriteLine(fmt.Sprintf("\n%s", 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, err := g.BehaviorStore.LoadGather(pd.behaviorID) if err != nil { continue } g.depleteSharedTree(st, cfg, pd.targetName, pd.playerNames) } g.pendingDepletions = nil } 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)) }