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": { Messages: []string{"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": { Messages: []string{"Smart choice. Hand over 500 credits and we'll forget this happened."}, Action: &behavior.Step{Credits: -500}, Options: []behavior.TalkOption{{Text: "\"Fine, take it.\""}}, }, "jail": { Messages: []string{"Off to the detention cell with you!"}, Action: &behavior.Step{Teleport: 162}, Options: []behavior.TalkOption{{Text: "(You are dragged away)"}}, }, "fight": { Messages: []string{"Then defend yourself!"}, Action: &behavior.Step{SetGlobalFlags: map[string]any{"guard_hostile": true}}, Options: []behavior.TalkOption{{Text: "(The guard attacks!)"}}, }, }, } 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 len(m.StealDrops) > 0 { 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.Steal == nil || len(def.Steal.Drops) == 0 { 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 stealDrops []behavior.DropEntry 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" { stealDrops = mob.StealDrops stealLevel = mob.StealLevel stealXP = mob.StealXP stealSpeed = mob.StealSpeed targetName = mob.Name targetID = mob.InstanceID mobInstanceID = mob.InstanceID if mob.OwnerOnly && mob.Owner != "" && mob.Owner != p.Name { sess.WriteLine("They don't seem interested in you.") return } if len(stealDrops) == 0 { 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 { stealDrops = objDef.Steal.Drops stealLevel = objDef.Steal.Level stealXP = objDef.Steal.XP stealSpeed = objDef.Steal.Speed targetName = objDef.Name targetID = objDef.ID objDefID = objDef.ID guardMob = objDef.Steal.GuardMob if len(stealDrops) == 0 { 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, "%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, StealDrops: stealDrops, 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 stealDrops := d.StealDrops 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 && objDef.Steal != nil { d.StealLevel = objDef.Steal.Level d.StealXP = objDef.Steal.XP d.StealSpeed = objDef.Steal.Speed stealLevel = objDef.Steal.Level stealXP = objDef.Steal.XP stealSpeed = objDef.Steal.Speed d.GuardMob = objDef.Steal.GuardMob guardMob = objDef.Steal.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 { if len(stealDrops) == 0 { sess.WriteLine("You steal nothing of value.") } else { drop := behavior.ResolveDrop(g.DataDir, stealDrops) 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, "currency_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) }