From 52a3ce6a4b4a254dc5d3067979a09e93c060fa20 Mon Sep 17 00:00:00 2001 From: workhorse Date: Fri, 19 Jun 2026 03:57:06 -0400 Subject: feat: shops and rough assassin skill --- internal/game/action_steal.go | 449 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 internal/game/action_steal.go (limited to 'internal/game/action_steal.go') diff --git a/internal/game/action_steal.go b/internal/game/action_steal.go new file mode 100644 index 0000000..dac90f2 --- /dev/null +++ b/internal/game/action_steal.go @@ -0,0 +1,449 @@ +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) +} -- cgit v1.2.3