From abd612c15799f604e671e83dc7c410ed2b44185f Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 25 Jun 2026 15:40:48 -0400 Subject: slop refactor --- internal/game/act_steal.go | 476 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 internal/game/act_steal.go (limited to 'internal/game/act_steal.go') 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) +} -- cgit v1.2.3