diff options
| author | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
| commit | abd612c15799f604e671e83dc7c410ed2b44185f (patch) | |
| tree | 0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/act_gather.go | |
| parent | 2725e2927a1595c7b100d942d1f14146252adeb7 (diff) | |
| download | thehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz | |
slop refactor
Diffstat (limited to 'internal/game/act_gather.go')
| -rw-r--r-- | internal/game/act_gather.go | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/internal/game/act_gather.go b/internal/game/act_gather.go new file mode 100644 index 0000000..0c743e7 --- /dev/null +++ b/internal/game/act_gather.go @@ -0,0 +1,405 @@ +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 + } + + 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 + var toolName string + + if len(cfg.Tools) > 0 { + toolSpeed, name, 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 + 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 + } + verb := cfg.Skill + switch cfg.Skill { + case "woodcutting": + verb = "chopping" + } + + g.broadcastAction(sess, "\n%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, + EffectiveWait: wait, + Verb: verb, + ToolName: toolName, + } + 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.EffectiveWait + 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.GatherMsg)) + 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, cfg.Level) + + if rand.Float64() < chance { + eligible := filterDropsByLevel(cfg.Drops, skillLevel) + 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 { + xp = cfg.XP + } + if xp > 0 { + g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), xp) + } + 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.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) + } + + for _, other := range g.Hub.PlayersInRoom(p.RoomID) { + if other == sess { + continue + } + 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(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) + } + } + + 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.FailMsg) + 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) + } + } + if len(eligible) == 0 { + return drops + } + return eligible +} + +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(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 := 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)) +} |
