From 1b9e2da3b3c438d8dc53d3489725dd5ba0022777 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 11 Jun 2026 04:46:27 -0400 Subject: feat: web client, get/drop updates and fixes --- internal/game/action_gather.go | 355 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 internal/game/action_gather.go (limited to 'internal/game/action_gather.go') diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go new file mode 100644 index 0000000..e1e724e --- /dev/null +++ b/internal/game/action_gather.go @@ -0,0 +1,355 @@ +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.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 { + p.AddXP(player.SkillName(cfg.Skill), xp) + } + g.AccountStore.SaveCharacter(p) + + msg := drop.Message + if msg == "" { + msg = fmt.Sprintf("You manage to get some %s.", itemName) + } + if xp > 0 && p.Toggles["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.DepleteDelay, 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, respawnTicks int, instanceKey string) { + st.Depleted = true + st.DepleteTimer = respawnTicks + st.SharedTimer = 0 + + sess.WriteLine(fmt.Sprintf("\nThe %s falls to the ground!", p.Action.TargetName)) + + 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 falls to the ground!", p.Action.TargetName)) + 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)) +} -- cgit v1.2.3