package game import ( "fmt" "math/rand" "sort" "strconv" "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) var stealSuccess = action.SuccessFormula{ Base: 0.5, PerLevel: 0.03, Cap: 0.95, } func (g *Game) doSteal(sess *net.Session, input string) { p := sess.Player.(*player.Player) if combat.GetCombat(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 world.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 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)) p.ActionState = &ActionState{Type: ActionStealing, TargetName: targetName} p.Action = &action.Action{ Type: "steal", TargetID: targetID, TargetName: targetName, WaitLeft: engine.ToTicks(stealSpeed), Data: map[string]any{ "target_type": targetType, "steal_table": stealTable, "steal_level": stealLevel, "steal_xp": stealXP, "steal_speed": stealSpeed, "target_name": targetName, "mob_instance_id": mobInstanceID, "obj_def_id": objDefID, "guard_mob": guardMob, "guard_watching": guardWatching, }, } } func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { data := p.Action.Data targetType := data["target_type"].(string) stealTable := data["steal_table"].(string) stealLevel := data["steal_level"].(int) stealXP := data["steal_xp"].(int) stealSpeed := data["steal_speed"].(float64) targetName := data["target_name"].(string) guardMob := data["guard_mob"].(string) guardWatching := data["guard_watching"].(bool) if targetType == "mob" { mobInstanceID := data["mob_instance_id"].(string) 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 combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName)) g.CancelAction(p) return } data["steal_level"] = mob.StealLevel data["steal_xp"] = mob.StealXP data["steal_speed"] = mob.StealSpeed stealLevel = mob.StealLevel stealXP = mob.StealXP stealSpeed = mob.StealSpeed } else { objDefID := data["obj_def_id"].(string) 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 { data["steal_level"] = objDef.StealLevel data["steal_xp"] = objDef.StealXP data["steal_speed"] = objDef.StealSpeed stealLevel = objDef.StealLevel stealXP = objDef.StealXP stealSpeed = objDef.StealSpeed data["guard_mob"] = 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) data["guard_watching"] = guardWatching } } } } level := p.Level(player.Thieving) chance := action.SuccessChance(stealSuccess, level, stealLevel) if guardWatching { chance *= 0.5 if chance < 0.05 { chance = 0.05 } } if rand.Float64() < chance { dt, err := g.BehaviorStore.LoadDropTable(stealTable) if err != nil || len(dt.Drops) == 0 { sess.WriteLine("You steal nothing of value.") } else { drop := g.BehaviorStore.ResolveDrop(dt.Drops) if drop == nil || drop.ItemID == "" { sess.WriteLine("You steal nothing of value.") } else { g.giveStealLoot(sess, p, drop) } } if stealXP > 0 { prevLevel := p.Level(player.Thieving) p.AddSkillXP(player.Thieving, stealXP) newLevel := p.Level(player.Thieving) if newLevel > prevLevel { sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d thieving! ***", newLevel))) } } 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 := data["mob_instance_id"].(string) 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 *action.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, err := g.BehaviorStore.LoadTalk("stall_guard_talk") if err != nil { sess.WriteLine("The Guard glares at you but says nothing.") return } node, ok := cfg.Nodes["start"] if !ok { return } p.ActionState = &ActionState{Type: ActionTalking, TargetName: guardMob.Name} p.Action = &action.Action{ Type: "talk", TargetID: guardMob.DefID, TargetName: guardMob.Name, Data: map[string]any{"node": "start", "behavior_id": "stall_guard_talk", "steal_guard": true}, } g.showTalkNode(sess, node) }