aboutsummaryrefslogtreecommitdiff
path: root/internal/game/act_steal.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 15:40:48 -0400
committerhistoria <[not public]>2026-06-25 15:40:48 -0400
commitabd612c15799f604e671e83dc7c410ed2b44185f (patch)
tree0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/act_steal.go
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/game/act_steal.go')
-rw-r--r--internal/game/act_steal.go476
1 files changed, 476 insertions, 0 deletions
diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go
new file mode 100644
index 0000000..6f67944
--- /dev/null
+++ b/internal/game/act_steal.go
@@ -0,0 +1,476 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "sort"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doSteal(sess, "")
+ } else {
+ g.doSteal(sess, strings.Join(args, " "))
+ }
+}
+
+var stallGuardTalk = &behavior.TalkConfig{
+ Nodes: map[string]behavior.TalkNode{
+ "start": {
+ Message: "Caught you red-handed! You have three options, thief.",
+ Options: []behavior.TalkOption{
+ {Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &behavior.Condition{MinCredits: 500}},
+ {Text: "\"Take me to jail.\"", Goto: "jail"},
+ {Text: "\"You'll have to catch me first!\"", Goto: "fight"},
+ },
+ },
+ "bribe": {
+ Message: "Smart choice. Hand over 500 credits and we'll forget this happened.",
+ Action: &behavior.NodeAction{Cost: 500},
+ Options: []behavior.TalkOption{{Text: "\"Fine, take it.\"", End: true}},
+ },
+ "jail": {
+ Message: "Off to the detention cell with you!",
+ Action: &behavior.NodeAction{Teleport: 162},
+ Options: []behavior.TalkOption{{Text: "(You are dragged away)", End: true}},
+ },
+ "fight": {
+ Message: "Then defend yourself!",
+ Action: &behavior.NodeAction{SetFlags: map[string]any{"guard_hostile": true}},
+ Options: []behavior.TalkOption{{Text: "(The guard attacks!)", End: true}},
+ },
+ },
+}
+
+var stealSuccess = behavior.SuccessFormula{
+ Base: 0.5,
+ PerLevel: 0.03,
+ Cap: 0.95,
+}
+
+func (g *Game) doSteal(sess *net.Session, input string) {
+ p := sess.Player
+
+ if g.Combat.Get(p.Name) != nil {
+ sess.WriteLine("You can't do that during combat!")
+ return
+ }
+
+ g.cancelAction(p)
+
+ targetType, mob, objDef, errMsg := g.resolveStealTarget(sess, p, input)
+ if errMsg != "" {
+ sess.WriteLine(errMsg)
+ return
+ }
+
+ g.startSteal(sess, p, targetType, mob, objDef)
+}
+
+func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input string) (targetType string, mob *world.MobInstance, objDef *object.ObjectDef, errMsg string) {
+ input = strings.ToLower(strings.TrimSpace(input))
+ instanceIdx := -1
+ searchName := input
+
+ if dotPos := strings.Index(input, "."); dotPos > 0 {
+ if n, err := strconv.Atoi(input[:dotPos]); err == nil && n > 0 {
+ instanceIdx = n - 1
+ searchName = input[dotPos+1:]
+ }
+ }
+
+ roomMobs := g.MobStore.MobsInRoom(p.RoomID)
+ var stealMobs []*world.MobInstance
+ for _, m := range roomMobs {
+ if m.StealTable != "" {
+ stealMobs = append(stealMobs, m)
+ }
+ }
+
+ roomObjs := g.World.AllObjInstances(p.RoomID)
+ var stealObjs []*object.ObjectDef
+ for _, st := range roomObjs {
+ def, err := g.ObjectStore.Load(st.DefID)
+ if err != nil || def.StealTable == "" {
+ continue
+ }
+ stealObjs = append(stealObjs, def)
+ }
+
+ if input == "" {
+ if len(stealMobs) == 0 && len(stealObjs) == 0 {
+ return "", nil, nil, "There's nothing here to steal from."
+ }
+
+ uniqueMobDefs := make(map[string]bool)
+ for _, m := range stealMobs {
+ uniqueMobDefs[m.DefID] = true
+ }
+
+ if len(stealMobs) > 0 && len(stealObjs) == 0 {
+ if len(uniqueMobDefs) <= 1 {
+ return "mob", stealMobs[0], nil, ""
+ }
+ }
+ if len(stealObjs) > 0 && len(stealMobs) == 0 {
+ uniqueObjDefs := make(map[string]bool)
+ for _, o := range stealObjs {
+ uniqueObjDefs[o.ID] = true
+ }
+ if len(uniqueObjDefs) <= 1 {
+ return "object", nil, stealObjs[0], ""
+ }
+ }
+ return "", nil, nil, "Steal from what?"
+ }
+
+ var matchingMobs []*world.MobInstance
+ for _, m := range stealMobs {
+ if m.MatchQuality(searchName) >= world.MatchPrefix {
+ matchingMobs = append(matchingMobs, m)
+ }
+ }
+ sort.Slice(matchingMobs, func(i, j int) bool {
+ return matchingMobs[i].InstanceID < matchingMobs[j].InstanceID
+ })
+
+ if len(matchingMobs) > 0 {
+ uniqueDefs := make(map[string]bool)
+ for _, m := range matchingMobs {
+ uniqueDefs[m.DefID] = true
+ }
+ if len(uniqueDefs) > 1 && instanceIdx < 0 {
+ return "", nil, nil, "Which one?"
+ }
+ if instanceIdx >= 0 && instanceIdx < len(matchingMobs) {
+ return "mob", matchingMobs[instanceIdx], nil, ""
+ }
+ if len(matchingMobs) == 1 {
+ return "mob", matchingMobs[0], nil, ""
+ }
+ for i := range matchingMobs {
+ if instanceIdx == i || instanceIdx < 0 {
+ return "mob", matchingMobs[i], nil, ""
+ }
+ }
+ }
+
+ var matchingObjs []*object.ObjectDef
+ for _, o := range stealObjs {
+ if behavior.WordPrefixMatch(searchName, o.Name) {
+ matchingObjs = append(matchingObjs, o)
+ }
+ }
+ if len(matchingObjs) > 1 {
+ return "", nil, nil, "Which one?"
+ }
+ if len(matchingObjs) == 1 {
+ return "object", nil, matchingObjs[0], ""
+ }
+
+ return "", nil, nil, "There's nothing here to steal from."
+}
+
+func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string, mob *world.MobInstance, objDef *object.ObjectDef) {
+ var stealTable string
+ var stealLevel int
+ var stealXP int
+ var stealSpeed float64
+ var targetName string
+ var targetID string
+ var mobInstanceID string
+ var objDefID string
+ var guardMob string
+ guardWatching := false
+
+ if targetType == "mob" {
+ stealTable = mob.StealTable
+ stealLevel = mob.StealLevel
+ stealXP = mob.StealXP
+ stealSpeed = mob.StealSpeed
+ targetName = mob.Name
+ targetID = mob.InstanceID
+ mobInstanceID = mob.InstanceID
+
+ if stealTable == "" {
+ sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName))
+ return
+ }
+ if thievingLevel := p.Level(player.Thieving); stealLevel > 0 && thievingLevel < stealLevel {
+ sess.WriteLine(fmt.Sprintf("You need level %d thieving to steal from the %s.", stealLevel, targetName))
+ return
+ }
+ if mob.HP <= 0 {
+ sess.WriteLine("That is already dead.")
+ return
+ }
+ if g.Combat.IsMobInCombat(mob.InstanceID) {
+ sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName))
+ return
+ }
+ } else {
+ stealTable = objDef.StealTable
+ stealLevel = objDef.StealLevel
+ stealXP = objDef.StealXP
+ stealSpeed = objDef.StealSpeed
+ targetName = objDef.Name
+ targetID = objDef.ID
+ objDefID = objDef.ID
+ guardMob = objDef.GuardMob
+
+ if stealTable == "" {
+ sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName))
+ return
+ }
+ if thievingLevel := p.Level(player.Thieving); stealLevel > 0 && thievingLevel < stealLevel {
+ sess.WriteLine(fmt.Sprintf("You need level %d thieving to steal from the %s.", stealLevel, targetName))
+ return
+ }
+
+ if guardMob != "" {
+ guard := g.findGuardInRoom(p.RoomID, guardMob)
+ if guard != nil {
+ timerKey := fmt.Sprintf("%d:%s", p.RoomID, objDefID)
+ if _, exists := g.guardWatchTimers[timerKey]; !exists {
+ g.guardWatchTimers[timerKey] = 0
+ }
+ guardWatching = g.isGuardWatching(p.RoomID, objDefID)
+ }
+ }
+ }
+
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ return
+ }
+
+ sess.WriteLine(fmt.Sprintf("You attempt to steal from the %s...", targetName))
+
+ g.broadcastAction(sess, "\n%s glances around the room.", p.Name)
+
+ p.Action = &behavior.Action{
+ Type: behavior.TypeSteal,
+ TargetID: targetID,
+ TargetName: targetName,
+ WaitLeft: engine.ToTicks(stealSpeed),
+ Data: &behavior.StealData{
+ TargetType: targetType,
+ StealTable: stealTable,
+ StealLevel: stealLevel,
+ StealXP: stealXP,
+ StealSpeed: stealSpeed,
+ TargetName: targetName,
+ MobInstanceID: mobInstanceID,
+ ObjDefID: objDefID,
+ GuardMob: guardMob,
+ GuardWatching: guardWatching,
+ },
+ }
+}
+
+func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
+ d := p.Action.Data.(*behavior.StealData)
+ targetType := d.TargetType
+ stealTable := d.StealTable
+ stealLevel := d.StealLevel
+ stealXP := d.StealXP
+ stealSpeed := d.StealSpeed
+ targetName := d.TargetName
+ guardMob := d.GuardMob
+ guardWatching := d.GuardWatching
+
+ if targetType == "mob" {
+ mobInstanceID := d.MobInstanceID
+ mob := g.MobStore.GetInstance(mobInstanceID)
+ if mob == nil || mob.HP <= 0 || mob.RoomID != p.RoomID {
+ sess.WriteLine("Your target is gone.")
+ g.cancelAction(p)
+ return
+ }
+ if g.Combat.IsMobInCombat(mob.InstanceID) {
+ sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName))
+ g.cancelAction(p)
+ return
+ }
+ d.StealLevel = mob.StealLevel
+ d.StealXP = mob.StealXP
+ d.StealSpeed = mob.StealSpeed
+ stealLevel = mob.StealLevel
+ stealXP = mob.StealXP
+ stealSpeed = mob.StealSpeed
+ } else {
+ objDefID := d.ObjDefID
+ found := false
+ for _, st := range g.World.AllObjInstances(p.RoomID) {
+ if st.DefID == objDefID && !st.Depleted {
+ found = true
+ break
+ }
+ }
+ if !found {
+ sess.WriteLine("Your target is gone.")
+ g.cancelAction(p)
+ return
+ }
+ objDef, err := g.ObjectStore.Load(objDefID)
+ if err == nil {
+ d.StealLevel = objDef.StealLevel
+ d.StealXP = objDef.StealXP
+ d.StealSpeed = objDef.StealSpeed
+ stealLevel = objDef.StealLevel
+ stealXP = objDef.StealXP
+ stealSpeed = objDef.StealSpeed
+ d.GuardMob = objDef.GuardMob
+ guardMob = objDef.GuardMob
+
+ if guardMob != "" {
+ guard := g.findGuardInRoom(p.RoomID, guardMob)
+ if guard != nil {
+ timerKey := fmt.Sprintf("%d:%s", p.RoomID, objDefID)
+ if _, exists := g.guardWatchTimers[timerKey]; !exists {
+ g.guardWatchTimers[timerKey] = 0
+ }
+ guardWatching = g.isGuardWatching(p.RoomID, objDefID)
+ d.GuardWatching = guardWatching
+ }
+ }
+ }
+ }
+
+ level := p.Level(player.Thieving)
+ chance := behavior.SuccessChance(stealSuccess, level, stealLevel)
+ if guardWatching {
+ chance *= 0.5
+ if chance < 0.05 {
+ chance = 0.05
+ }
+ }
+
+ if rand.Float64() < chance {
+ dt, err := behavior.LoadDropTable(g.DataDir, stealTable)
+ if err != nil || len(dt.Drops) == 0 {
+ sess.WriteLine("You steal nothing of value.")
+ } else {
+ drop := behavior.ResolveDrop(g.DataDir, dt.Drops)
+ if drop == nil || drop.ItemID == "" {
+ sess.WriteLine("You steal nothing of value.")
+ } else {
+ g.giveStealLoot(sess, p, drop)
+ }
+ }
+
+ if stealXP > 0 {
+ g.awardSkillXP(sess, p, player.Thieving, stealXP)
+ }
+
+ g.AccountStore.SaveCharacter(p)
+
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is full.")
+ g.cancelAction(p)
+ return
+ }
+
+ p.Action.WaitLeft = engine.ToTicks(stealSpeed)
+ } else {
+ if targetType == "mob" {
+ mobInstanceID := d.MobInstanceID
+ mob := g.MobStore.GetInstance(mobInstanceID)
+ sess.WriteLine(fmt.Sprintf("The %s notices you! They attack!", targetName))
+ g.cancelAction(p)
+ if mob != nil && mob.HP > 0 {
+ g.startCombat(sess, p, mob)
+ }
+ } else {
+ if guardMob != "" && guardWatching {
+ guard := g.findGuardInRoom(p.RoomID, guardMob)
+ if guard != nil {
+ sess.WriteLine("You fumble and the Guard spots you!")
+ g.cancelAction(p)
+ g.startStealGuardTalk(sess, p, guard)
+ return
+ }
+ }
+ sess.WriteLine("You fail to steal anything.")
+ p.Action.WaitLeft = engine.ToTicks(stealSpeed)
+ }
+ }
+}
+
+func (g *Game) giveStealLoot(sess *net.Session, p *player.Player, drop *behavior.DropEntry) {
+ qty := drop.Quantity
+ if qty <= 0 {
+ qty = 1
+ }
+
+ if drop.ItemID == "credits" {
+ p.Credits += qty
+ sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You steal %d credits.", qty)))
+ return
+ }
+
+ name := drop.ItemID
+ lootDef, _ := g.ItemStore.Load(drop.ItemID)
+ if lootDef != nil {
+ name = lootDef.Name
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ g.World.AddGroundItem(p.RoomID, drop.ItemID, qty)
+ sess.WriteLine(fmt.Sprintf("You steal %s but your inventory is full. It falls to the ground.", g.itemColorize(sess, lootDef, name)))
+ return
+ }
+
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty})
+ sess.WriteLine(fmt.Sprintf("You steal %s.", g.itemColorize(sess, lootDef, name)))
+}
+
+func (g *Game) findGuardInRoom(roomID int, guardDefID string) *world.MobInstance {
+ mobs := g.MobStore.MobsInRoom(roomID)
+ for _, m := range mobs {
+ if m.DefID == guardDefID && m.HP > 0 {
+ return m
+ }
+ }
+ return nil
+}
+
+const guardWatchDuration = 8
+const guardLookAwayDuration = 4
+const guardCycleLength = guardWatchDuration + guardLookAwayDuration
+
+func (g *Game) isGuardWatching(roomID int, objDefID string) bool {
+ key := fmt.Sprintf("%d:%s", roomID, objDefID)
+ counter := g.guardWatchTimers[key]
+ return counter%guardCycleLength < guardWatchDuration
+}
+
+func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob *world.MobInstance) {
+ cfg := stallGuardTalk
+ node, ok := cfg.Nodes["start"]
+ if !ok {
+ sess.WriteLine("The Guard glares at you but says nothing.")
+ return
+ }
+
+ p.Action = &behavior.Action{
+ Type: "talk",
+ TargetID: guardMob.DefID,
+ TargetName: guardMob.Name,
+ Data: &behavior.TalkData{Node: "start", Cfg: cfg, StealGuard: true},
+ }
+
+ g.showTalkNode(sess, node)
+}