aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action_steal.go
diff options
context:
space:
mode:
authorworkhorse <workhorse@localhost.localdomain>2026-06-19 03:57:06 -0400
committerworkhorse <workhorse@localhost.localdomain>2026-06-19 03:57:06 -0400
commit52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (patch)
tree167c62990013733bc8669c1387078bc86ea48db5 /internal/game/action_steal.go
parentfbd1c30d7b9777c9df6653f0c393afa7a7dfa519 (diff)
downloadthehouseoficarus-52a3ce6a4b4a254dc5d3067979a09e93c060fa20.tar.gz
feat: shops and rough assassin skill
Diffstat (limited to 'internal/game/action_steal.go')
-rw-r--r--internal/game/action_steal.go449
1 files changed, 449 insertions, 0 deletions
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)
+}