aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action_gather.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-11 04:46:27 -0400
committerhistoria <[not public]>2026-06-11 08:58:37 +0000
commit1b9e2da3b3c438d8dc53d3489725dd5ba0022777 (patch)
tree62264f24420bf4cfaa734942c65cc8dd45ba3d3b /internal/game/action_gather.go
parent06c02a697e5daf8832a462de5970dd4c0e13a02c (diff)
downloadthehouseoficarus-1b9e2da3b3c438d8dc53d3489725dd5ba0022777.tar.gz
feat: web client, get/drop updates and fixes
Diffstat (limited to 'internal/game/action_gather.go')
-rw-r--r--internal/game/action_gather.go355
1 files changed, 355 insertions, 0 deletions
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))
+}