aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/action.go6
-rw-r--r--internal/game/action_state.go3
-rw-r--r--internal/game/action_steal.go449
-rw-r--r--internal/game/action_talk.go47
-rw-r--r--internal/game/assassin.go284
-rw-r--r--internal/game/cmd_attack.go54
-rw-r--r--internal/game/cmd_shop.go295
-rw-r--r--internal/game/cmd_sneak.go71
-rw-r--r--internal/game/cmd_task.go34
-rw-r--r--internal/game/game.go30
10 files changed, 1269 insertions, 4 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
index 2d30010..3cc51c6 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -25,6 +25,8 @@ var verbAliases = map[string]string{
"ask": "talk",
"pull": "toggle",
"push": "toggle",
+ "steal": "steal",
+ "thieve": "steal",
}
var verbSkill = map[string]string{
@@ -33,6 +35,8 @@ var verbSkill = map[string]string{
"cut": "woodcutting",
"fish": "fishing",
"shear": "crafting",
+ "steal": "thieving",
+ "thieve": "thieving",
}
func normalizeVerb(v string) string {
@@ -235,6 +239,8 @@ func (g *Game) AdvanceActions() {
g.advanceSearch(sess, p)
case "identify":
g.advanceIdentify(sess, p)
+ case "steal":
+ g.advanceSteal(sess, p)
default:
if productionActionTypes[p.Action.Type] {
g.advanceProduction(sess, p)
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 596eb4e..2dc42ae 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -26,6 +26,7 @@ const (
ActionMixing ActionType = "mixing"
ActionIdentifying ActionType = "identifying"
ActionTriggering ActionType = "triggering"
+ ActionStealing ActionType = "stealing"
)
type ActionState struct {
@@ -85,6 +86,8 @@ func (a *ActionState) Description() string {
return "identifying scrap at " + a.TargetName
case ActionTriggering:
return "triggering " + a.TargetName
+ case ActionStealing:
+ return "stealing from " + a.TargetName
}
return ""
}
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)
+}
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index 2d4a814..1a21e7f 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -57,7 +57,27 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", node.Message)))
if node.Action != nil {
+ if node.Action.Shop != nil {
+ g.applyNodeAction(sess, node.Action)
+ g.enterShop(sess, node.Action.Shop)
+ return
+ }
g.applyNodeAction(sess, node.Action)
+
+ if g.WorldFlags["guard_hostile"] != nil {
+ delete(g.WorldFlags, "guard_hostile")
+ p, _ := sess.Player.(*player.Player)
+ if p != nil && p.Action != nil && p.Action.Data["steal_guard"] == true {
+ guardMob := g.findGuardInRoom(p.RoomID, "guard")
+ if guardMob != nil {
+ sess.State = net.StateGame
+ g.CancelAction(p)
+ guardMob.Protected = false
+ g.startCombat(sess, p, guardMob)
+ }
+ return
+ }
+ }
}
if len(node.Options) == 0 {
@@ -162,11 +182,38 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
g.writePrompt(sess)
}
+func (g *Game) enterShop(sess *net.Session, cfg *action.ShopConfig) {
+ sess.Shop = cfg
+ sess.State = net.StateShop
+ g.showShopBrowse(sess)
+ g.writeShopPrompt(sess)
+}
+
func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
p, ok := sess.Player.(*player.Player)
if !ok {
return
}
+
+ if na.AssignTask {
+ g.assignAssassinTask(sess, p)
+ }
+ if na.SkipTask {
+ g.skipAssassinTask(sess, p)
+ }
+ if na.ExtendTask {
+ g.extendAssassinTask(sess, p)
+ }
+ if na.ReputationCost > 0 {
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+ if rep < na.ReputationCost {
+ sess.WriteLine(fmt.Sprintf("You don't have enough Reputation. (Need %d, have %d)", na.ReputationCost, rep))
+ return
+ }
+ setPlayerFlag(p, "assassin_reputation", rep-na.ReputationCost)
+ g.AccountStore.SaveCharacter(p)
+ }
+
for k, v := range na.SetFlags {
g.WorldFlags[k] = v
}
diff --git a/internal/game/assassin.go b/internal/game/assassin.go
new file mode 100644
index 0000000..d7af1a4
--- /dev/null
+++ b/internal/game/assassin.go
@@ -0,0 +1,284 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+func getPlayerFlagInt(p *player.Player, key string) int {
+ if p.Flags == nil {
+ return 0
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return 0
+ }
+ switch v := val.(type) {
+ case int:
+ return v
+ case int64:
+ return int(v)
+ case float64:
+ return int(v)
+ }
+ return 0
+}
+
+func getPlayerFlagString(p *player.Player, key string) string {
+ if p.Flags == nil {
+ return ""
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return ""
+ }
+ s, _ := val.(string)
+ return s
+}
+
+func setPlayerFlag(p *player.Player, key string, val any) {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ p.Flags[key] = val
+}
+
+type assassinTaskEntry struct {
+ MobID string
+ MinLevel int
+ MaxLevel int
+ MinCount int
+ MaxCount int
+ Weight int
+}
+
+var assassinTaskTable = []assassinTaskEntry{
+ {"man", 1, 15, 10, 25, 8},
+ {"cow", 1, 15, 10, 25, 8},
+ {"slug", 1, 99, 15, 45, 15},
+ {"drone", 15, 99, 20, 50, 12},
+ {"crawler", 30, 99, 15, 40, 10},
+ {"phantom", 45, 99, 10, 30, 8},
+}
+
+func (g *Game) assignAssassinTask(sess *net.Session, p *player.Player) {
+ level := p.Level(player.Assassin)
+ var eligible []assassinTaskEntry
+ totalWeight := 0
+ for _, entry := range assassinTaskTable {
+ if level >= entry.MinLevel && level <= entry.MaxLevel {
+ eligible = append(eligible, entry)
+ totalWeight += entry.Weight
+ }
+ }
+ if len(eligible) == 0 {
+ sess.WriteLine("The Client shakes their head. \"Nothing available for your level.\"")
+ return
+ }
+ roll := rand.Intn(totalWeight)
+ var chosen assassinTaskEntry
+ for _, entry := range eligible {
+ roll -= entry.Weight
+ if roll < 0 {
+ chosen = entry
+ break
+ }
+ }
+ count := chosen.MinCount + rand.Intn(chosen.MaxCount-chosen.MinCount+1)
+ setPlayerFlag(p, "assassin_task_mob", chosen.MobID)
+ setPlayerFlag(p, "assassin_task_total", count)
+ setPlayerFlag(p, "assassin_task_remaining", count)
+ g.AccountStore.SaveCharacter(p)
+
+ def, err := g.MobStore.LoadDef(chosen.MobID)
+ name := chosen.MobID
+ if err == nil {
+ name = def.Name
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\"Your target: %d %ss. Get to work.\"", count, name)))
+}
+
+func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ taskMob := getPlayerFlagString(p, "assassin_task_mob")
+ if taskMob == "" || taskMob != mob.DefID {
+ return
+ }
+ remaining := getPlayerFlagInt(p, "assassin_task_remaining")
+ if remaining <= 0 {
+ return
+ }
+ xp := mob.MaxHP * 2
+ newLevel := p.AddSkillXP(player.Assassin, xp)
+ if newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d Assassin! ***", newLevel)))
+ }
+ if p.OptionBool("xp_drops") {
+ sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp asm)", xp)))
+ }
+ remaining--
+ setPlayerFlag(p, "assassin_task_remaining", remaining)
+ if remaining <= 0 {
+ completed := getPlayerFlagInt(p, "assassin_tasks_completed") + 1
+ streak := getPlayerFlagInt(p, "assassin_streak") + 1
+ setPlayerFlag(p, "assassin_tasks_completed", completed)
+ setPlayerFlag(p, "assassin_streak", streak)
+ delete(p.Flags, "assassin_task_mob")
+ setPlayerFlag(p, "assassin_task_remaining", 0)
+ setPlayerFlag(p, "assassin_task_total", 0)
+ rep := 1
+ bonus := streakBonus(streak)
+ rep += bonus
+ currentRep := getPlayerFlagInt(p, "assassin_reputation")
+ setPlayerFlag(p, "assassin_reputation", currentRep+rep)
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\n*** Assassin task complete! ***")))
+ if bonus > 0 {
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Streak bonus! %d tasks in a row. +%d bonus reputation.", streak, bonus)))
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Reputation earned: %d (total: %d)", rep, currentRep+rep)))
+ } else {
+ total := getPlayerFlagInt(p, "assassin_task_total")
+ def, _ := g.MobStore.LoadDef(taskMob)
+ name := taskMob
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Assassin task: %d of %d %ss remaining.", remaining, total, name)))
+ }
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) skipAssassinTask(sess *net.Session, p *player.Player) {
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+ if rep < 30 {
+ sess.WriteLine("You don't have enough Reputation to skip. (Need 30, have " + fmt.Sprint(rep) + ")")
+ return
+ }
+ setPlayerFlag(p, "assassin_reputation", rep-30)
+ delete(p.Flags, "assassin_task_mob")
+ setPlayerFlag(p, "assassin_task_remaining", 0)
+ setPlayerFlag(p, "assassin_task_total", 0)
+ setPlayerFlag(p, "assassin_streak", 0)
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) extendAssassinTask(sess *net.Session, p *player.Player) {
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+ if rep < 30 {
+ sess.WriteLine("You don't have enough Reputation to extend. (Need 30, have " + fmt.Sprint(rep) + ")")
+ return
+ }
+ taskMob := getPlayerFlagString(p, "assassin_task_mob")
+ if taskMob == "" {
+ sess.WriteLine("You don't have an active task to extend.")
+ return
+ }
+ setPlayerFlag(p, "assassin_reputation", rep-30)
+ total := getPlayerFlagInt(p, "assassin_task_total")
+ remaining := getPlayerFlagInt(p, "assassin_task_remaining")
+ extension := total / 2
+ if extension < 5 {
+ extension = 5
+ }
+ setPlayerFlag(p, "assassin_task_total", total+extension)
+ setPlayerFlag(p, "assassin_task_remaining", remaining+extension)
+ g.AccountStore.SaveCharacter(p)
+
+ def, _ := g.MobStore.LoadDef(taskMob)
+ name := taskMob
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("Task extended by %d. Kill %d more %ss (%d total).", extension, remaining+extension, name, total+extension))
+}
+
+func streakBonus(streak int) int {
+ if streak%1000 == 0 {
+ return 50
+ }
+ if streak%250 == 0 {
+ return 35
+ }
+ if streak%100 == 0 {
+ return 25
+ }
+ if streak%50 == 0 {
+ return 15
+ }
+ if streak%10 == 0 {
+ return 5
+ }
+ return 0
+}
+
+func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool {
+ p := sess.Player.(*player.Player)
+ cs := combat.GetCombat(p.Name)
+ if cs == nil {
+ return false
+ }
+ mob := g.MobStore.GetInstance(cs.MobID)
+ if mob == nil || mob.FinishingBlow == "" || mob.HP != 1 {
+ return false
+ }
+ lower := strings.ToLower(input)
+ var itemPart, targetPart string
+ for _, sep := range []string{" on ", " with "} {
+ if idx := strings.Index(lower, sep); idx > 0 {
+ itemPart = strings.TrimSpace(input[:idx])
+ targetPart = strings.TrimSpace(input[idx+len(sep):])
+ break
+ }
+ }
+ if targetPart == "" {
+ itemPart = strings.TrimSpace(input)
+ if mob.MatchQuality(itemPart) != world.MatchNone {
+ targetPart = itemPart
+ itemPart = strings.TrimSpace(mob.FinishingBlow)
+ }
+ }
+ if targetPart == "" {
+ return false
+ }
+ if mob.MatchQuality(targetPart) == world.MatchNone {
+ return false
+ }
+ g.doFinishingBlow(sess, p, mob, itemPart)
+ return true
+}
+
+func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.MobInstance, itemInput string) {
+ fbDef, err := g.ItemStore.Load(mob.FinishingBlow)
+ if err != nil {
+ sess.WriteLine("Something went wrong.")
+ return
+ }
+ if !fbDef.MatchesName(itemInput) && !strings.EqualFold(itemInput, fbDef.ID) {
+ sess.WriteLine(fmt.Sprintf("That won't work on %s. You need %s.", mobDisplayName(mob, true), fbDef.Name))
+ return
+ }
+ if !p.HasItem(mob.FinishingBlow) {
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", fbDef.Name))
+ return
+ }
+ autoKey := "assassin_unlocked_auto_" + mob.FinishingBlow
+ consumed := true
+ if p.Flags != nil {
+ if val, ok := p.Flags[autoKey]; ok {
+ if b, ok := val.(bool); ok && b {
+ consumed = false
+ }
+ }
+ }
+ if consumed {
+ p.RemoveItem(mob.FinishingBlow, 1)
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true))))
+ mob.HP = 0
+ g.endCombat(sess, p, mob)
+}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 29bff30..a8dc5d3 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -41,6 +41,11 @@ func (g *Game) doAttack(sess *net.Session, input string) {
return
}
+ if mob.AssassinLevel > 0 && p.Level(player.Assassin) < mob.AssassinLevel {
+ sess.WriteLine(fmt.Sprintf("You need Assassin level %d to attack %s.", mob.AssassinLevel, mobDisplayName(mob, true)))
+ return
+ }
+
if combat.IsMobInCombat(mob.InstanceID) {
sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false)))
return
@@ -268,9 +273,23 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
if mob.HP < 0 {
mob.HP = 0
}
+ if mob.FinishingBlow != "" && mob.HP <= 0 {
+ mob.HP = 1
+ }
if mob.HP < mob.MaxHP && mob.HP > 0 {
mob.StartRegen()
}
+
+ if mob.FinishingBlow != "" && mob.HP == 1 {
+ fbDef, _ := g.ItemStore.Load(mob.FinishingBlow)
+ fbName := mob.FinishingBlow
+ if fbDef != nil {
+ fbName = fbDef.Name
+ }
+ sess.WriteLine(g.colorize(sess, "warning",
+ fmt.Sprintf(" %s resists death! Use %s on it to finish it off.",
+ mobDisplayName(mob, false), fbName)))
+ }
gains, leveled := g.awardCombatXP(p, dmg, isRanged)
if isRanged {
@@ -329,6 +348,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
}
func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ cs := combat.GetCombat(p.Name)
_, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
mobAttackType := mob.AttackType
@@ -349,6 +369,37 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
dmg := combat.RollDamage(maxHit)
dmg = g.applyTechProtection(p, mob, dmg)
+ if mob.DamageWithout != "" {
+ hasProtection := false
+ for _, itemID := range p.Equipment {
+ if itemID == mob.DamageWithout {
+ hasProtection = true
+ break
+ }
+ }
+ if !hasProtection {
+ dmg = dmg * 3 / 2
+ if dmg < 1 {
+ dmg = 1
+ }
+ if cs != nil && !cs.DamageWarningShown {
+ cs.DamageWarningShown = true
+ fbDef, _ := g.ItemStore.Load(mob.DamageWithout)
+ fbName := mob.DamageWithout
+ if fbDef != nil {
+ fbName = fbDef.Name
+ }
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ sess.WriteLine(g.colorize(sess, "warning",
+ fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.",
+ attacker, fbName)))
+ }
+ }
+ }
+
p.HP -= dmg
if p.HP < 0 {
p.HP = 0
@@ -421,6 +472,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if mob != nil && mob.HP <= 0 {
sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true))))
+
+ g.onAssassinKill(sess, p, mob)
+
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
if other != sess && other.Player != nil {
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
new file mode 100644
index 0000000..a3f7613
--- /dev/null
+++ b/internal/game/cmd_shop.go
@@ -0,0 +1,295 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) handleShopInput(sess *net.Session, input string) {
+ if sess.Player == nil {
+ sess.State = net.StateGame
+ g.writePrompt(sess)
+ return
+ }
+
+ input = strings.TrimSpace(input)
+ parts := strings.Fields(strings.ToLower(input))
+ if len(parts) == 0 {
+ g.showShopBrowse(sess)
+ return
+ }
+
+ cmd := parts[0]
+
+ switch cmd {
+ case "buy":
+ if len(parts) < 2 {
+ sess.WriteLine("Buy what? Use 'browse' to see available items.")
+ } else {
+ g.doShopBuy(sess, strings.Join(parts[1:], " "))
+ }
+ case "sell":
+ if len(parts) < 2 {
+ sess.WriteLine("Sell what?")
+ } else {
+ g.doShopSell(sess, strings.Join(parts[1:], " "))
+ }
+ case "browse", "list":
+ g.showShopBrowse(sess)
+ case "leave", "bye", "exit", "quit":
+ g.leaveShop(sess)
+ return
+ default:
+ sess.WriteLine("Commands: buy <item>, sell <item>, browse, leave")
+ }
+ g.writeShopPrompt(sess)
+}
+
+func (g *Game) writeShopPrompt(sess *net.Session) {
+ sess.Write(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", "> ")))
+}
+
+func (g *Game) shopConfig(sess *net.Session) *action.ShopConfig {
+ cfg, _ := sess.Shop.(*action.ShopConfig)
+ return cfg
+}
+
+func (g *Game) showShopBrowse(sess *net.Session) {
+ cfg := g.shopConfig(sess)
+ if cfg == nil {
+ return
+ }
+
+ if cfg.Message != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", cfg.Message)))
+ }
+
+ if len(cfg.Items) == 0 {
+ sess.WriteLine("This shop has nothing for sale.")
+ return
+ }
+
+ unicode := true
+ if p, ok := sess.Player.(*player.Player); ok {
+ unicode = p.OptionBool("unicode")
+ }
+
+ t := Table{
+ Title: "Shop Inventory",
+ Columns: []string{"#", "Item", "Buy Price", "Sell Price"},
+ }
+
+ for i, si := range cfg.Items {
+ def, _ := g.ItemStore.Load(si.ItemID)
+ itemName := si.ItemID
+ if def != nil {
+ itemName = def.Name
+ }
+ buyStr := fmt.Sprintf("%d cr", si.BuyPrice)
+ sellStr := "\u2014"
+ if si.SellPrice > 0 {
+ sellStr = fmt.Sprintf("%d cr", si.SellPrice)
+ }
+ t.Rows = append(t.Rows, []string{
+ fmt.Sprintf("%d", i+1),
+ g.colorize(sess, "item", itemName),
+ g.colorize(sess, "credits_pickup", buyStr),
+ g.colorize(sess, "credits_pickup", sellStr),
+ })
+ }
+
+ for _, line := range t.Render(unicode) {
+ sess.WriteLine(line)
+ }
+}
+
+func (g *Game) doShopBuy(sess *net.Session, input string) {
+ cfg := g.shopConfig(sess)
+ if cfg == nil {
+ return
+ }
+
+ idx, err := parseChoiceIndex(input)
+ if err == nil && idx > 0 && idx <= len(cfg.Items) {
+ si := cfg.Items[idx-1]
+ g.buyShopItem(sess, si)
+ return
+ }
+
+ var match *action.ShopItem
+ for i := range cfg.Items {
+ si := &cfg.Items[i]
+ def, _ := g.ItemStore.Load(si.ItemID)
+ itemName := si.ItemID
+ if def != nil {
+ itemName = def.Name
+ }
+
+ invMatches := g.collectMatches(input, func(yield func(string, int) bool) {
+ yield(itemName, -1)
+ })
+
+ if len(invMatches) > 0 {
+ if match != nil {
+ sess.WriteLine(fmt.Sprintf("That's ambiguous, which one? (use #, e.g. buy %s 1)", si.ItemID))
+ return
+ }
+ match = si
+ }
+ }
+
+ if match != nil {
+ g.buyShopItem(sess, *match)
+ return
+ }
+
+ sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.")
+}
+
+func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) {
+ p, _ := sess.Player.(*player.Player)
+
+ if p.Credits < si.BuyPrice {
+ sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
+ return
+ }
+
+ def, _ := g.ItemStore.Load(si.ItemID)
+ displayName := si.ItemID
+ if def != nil {
+ displayName = def.Name
+ }
+
+ if def != nil && def.Stackable {
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == si.ItemID {
+ p.Credits -= si.BuyPrice
+ slot.Quantity++
+ g.AccountStore.SaveCharacter(p)
+ itemColor := g.itemColorize(sess, def, displayName)
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ return
+ }
+ }
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is full.")
+ return
+ }
+
+ p.Credits -= si.BuyPrice
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: si.ItemID, Quantity: 1})
+ g.AccountStore.SaveCharacter(p)
+
+ itemColor := g.itemColorize(sess, def, displayName)
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+}
+
+func (g *Game) doShopSell(sess *net.Session, input string) {
+ cfg := g.shopConfig(sess)
+ if cfg == nil {
+ return
+ }
+
+ p, _ := sess.Player.(*player.Player)
+
+ matches := g.findInventoryMatches(input, p)
+ if len(matches) == 0 {
+ sess.WriteLine("You don't have that item.")
+ return
+ }
+
+ if len(matches) > 1 {
+ sess.WriteLine("That's ambiguous, which one?")
+ return
+ }
+
+ match := matches[0]
+
+ var shopItem *action.ShopItem
+ for i := range cfg.Items {
+ si := &cfg.Items[i]
+ if si.ItemID == match.ID && si.SellPrice > 0 {
+ shopItem = si
+ break
+ }
+ }
+
+ if shopItem == nil {
+ def, _ := g.ItemStore.Load(match.ID)
+ displayName := match.ID
+ if def != nil {
+ displayName = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("The shop doesn't want to buy %s.", g.colorize(sess, "item", displayName)))
+ return
+ }
+
+ p.RemoveItem(match.ID, 1)
+ p.Credits += shopItem.SellPrice
+ g.AccountStore.SaveCharacter(p)
+
+ def, _ := g.ItemStore.Load(match.ID)
+ displayName := match.ID
+ if def != nil {
+ displayName = def.Name
+ }
+ itemColor := g.itemColorize(sess, def, displayName)
+ sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice))))
+}
+
+func (g *Game) leaveShop(sess *net.Session) {
+ sess.Shop = nil
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil || p.Action == nil || p.Action.Type != "talk" {
+ sess.State = net.StateGame
+ if p != nil {
+ g.CancelAction(p)
+ }
+ g.writePrompt(sess)
+ return
+ }
+
+ behaviorID, _ := p.Action.Data["behavior_id"].(string)
+ nodeKey, _ := p.Action.Data["node"].(string)
+
+ cfg, err := g.BehaviorStore.LoadTalk(behaviorID)
+ if err != nil {
+ sess.State = net.StateGame
+ g.CancelAction(p)
+ g.writePrompt(sess)
+ return
+ }
+
+ node, ok := cfg.Nodes[nodeKey]
+ if !ok {
+ sess.State = net.StateGame
+ g.CancelAction(p)
+ g.writePrompt(sess)
+ return
+ }
+
+ sess.State = net.StateTalk
+ visible := 0
+ for _, opt := range node.Options {
+ if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) {
+ continue
+ }
+ visible++
+ sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text))
+ }
+ if visible == 0 {
+ sess.State = net.StateGame
+ g.CancelAction(p)
+ g.writePrompt(sess)
+ return
+ }
+ sess.Write("\nChoice: ")
+}
diff --git a/internal/game/cmd_sneak.go b/internal/game/cmd_sneak.go
new file mode 100644
index 0000000..97aab8a
--- /dev/null
+++ b/internal/game/cmd_sneak.go
@@ -0,0 +1,71 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doSneak(sess *net.Session) {
+ p := sess.Player.(*player.Player)
+ p.Sneaking = !p.Sneaking
+ if p.Sneaking {
+ p.SneakNotified = make(map[string]bool)
+ sess.WriteLine("You begin sneaking.")
+ } else {
+ p.SneakNotified = nil
+ sess.WriteLine("You stop sneaking.")
+ }
+}
+
+func (g *Game) SneakTick() {
+ if g.Hub == nil {
+ return
+ }
+
+ for key := range g.guardWatchTimers {
+ g.guardWatchTimers[key]++
+ }
+
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil || !p.Sneaking {
+ continue
+ }
+
+ objs := g.World.AllObjInstances(p.RoomID)
+ for _, st := range objs {
+ objDef, err := g.ObjectStore.Load(st.DefID)
+ if err != nil || objDef.GuardMob == "" {
+ continue
+ }
+
+ guard := g.findGuardInRoom(p.RoomID, objDef.GuardMob)
+ if guard == nil {
+ continue
+ }
+
+ timerKey := fmt.Sprintf("%d:%s", p.RoomID, st.DefID)
+ if _, exists := g.guardWatchTimers[timerKey]; !exists {
+ g.guardWatchTimers[timerKey] = 0
+ }
+
+ watching := g.isGuardWatching(p.RoomID, st.DefID)
+
+ if p.SneakNotified == nil {
+ p.SneakNotified = make(map[string]bool)
+ }
+
+ lastState, known := p.SneakNotified[timerKey]
+ if !known || lastState != watching {
+ if watching {
+ sess.WriteLine(fmt.Sprintf("The %s is watching the %s.", guard.Name, objDef.Name))
+ } else {
+ sess.WriteLine(fmt.Sprintf("The %s looks away from the %s.", guard.Name, objDef.Name))
+ }
+ p.SneakNotified[timerKey] = watching
+ }
+ }
+ }
+}
diff --git a/internal/game/cmd_task.go b/internal/game/cmd_task.go
new file mode 100644
index 0000000..2dcf3f4
--- /dev/null
+++ b/internal/game/cmd_task.go
@@ -0,0 +1,34 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doTask(sess *net.Session) {
+ p := sess.Player.(*player.Player)
+
+ mobID := getPlayerFlagString(p, "assassin_task_mob")
+ remaining := getPlayerFlagInt(p, "assassin_task_remaining")
+ total := getPlayerFlagInt(p, "assassin_task_total")
+ streak := getPlayerFlagInt(p, "assassin_streak")
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+
+ if mobID == "" || remaining <= 0 {
+ if getPlayerFlagInt(p, "assassin_tasks_completed") > 0 {
+ sess.WriteLine("You have no active task. Talk to The Client for a new assignment.")
+ } else {
+ sess.WriteLine("You don't have an Assassin task. Talk to The Client to get one.")
+ }
+ } else {
+ def, err := g.MobStore.LoadDef(mobID)
+ name := mobID
+ if err == nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("Assassin task: Kill %ss. %d of %d remaining.", name, remaining, total))
+ }
+ sess.WriteLine(fmt.Sprintf("Streak: %d | Reputation: %d", streak, rep))
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index 9d8a2c9..8b99731 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -52,8 +52,9 @@ type Game struct {
combatPadWidth int
freeQueue map[string][]QueuedCommand
activeQueue map[string]*QueuedCommand
- consumeQueue map[string]*QueuedCommand
+ consumeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
+ guardWatchTimers map[string]int
}
func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
@@ -75,6 +76,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
activeQueue: make(map[string]*QueuedCommand),
consumeQueue: make(map[string]*QueuedCommand),
pendingDepletions: nil,
+ guardWatchTimers: make(map[string]int),
}
}
@@ -121,6 +123,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleDescriptionChange(sess, input)
case net.StateTalk:
g.handleTalkInput(sess, input)
+ case net.StateShop:
+ g.handleShopInput(sess, input)
case net.StateDropAllConfirm:
g.handleDropAllConfirm(sess, input)
case net.StateRecipeChoice:
@@ -141,8 +145,9 @@ func classifyCommand(cmd string) CommandClass {
"map", "option", "options", "alias", "unalias",
"description", "desc", "queued", "color", "colors",
"colortable", "prompt", "style", "stats",
- "tech", "t",
- "autocast", "auto", "mods", "modlist":
+ "tech", "t", "sneak",
+ "autocast", "auto", "mods", "modlist",
+ "task":
return ClassInstant
case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
@@ -152,6 +157,7 @@ func classifyCommand(cmd string) CommandClass {
"west", "w", "up", "u", "down", "d",
"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix",
"id", "identify",
+ "steal", "thieve",
"trigger", "cast":
return ClassActive
case "eat", "fletch", "clean":
@@ -319,6 +325,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
}
case "sc", "score":
g.doScore(sess)
+ case "task":
+ g.doTask(sess)
case "stats":
g.doStats(sess)
case "tech", "t":
@@ -374,6 +382,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
return
}
case "use":
+ if g.tryFinishingBlow(sess, strings.Join(args, " ")) {
+ return
+ }
g.doUse(sess, strings.Join(args, " "))
return
case "cook":
@@ -407,6 +418,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doAutocast(sess, strings.Join(args, " "))
case "mods", "modlist":
g.doMods(sess)
+ case "sneak":
+ g.doSneak(sess)
case "trigger", "cast":
g.doTrigger(sess, strings.Join(args, " "))
return
@@ -440,6 +453,14 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doSearch(sess, strings.Join(args, " "))
}
return
+ case "steal", "thieve":
+ g.CancelAction(p)
+ if len(args) == 0 {
+ g.doSteal(sess, "")
+ } else {
+ g.doSteal(sess, strings.Join(args, " "))
+ }
+ return
case "walk":
g.doWalk(sess, args)
return
@@ -495,7 +516,8 @@ func (g *Game) ProcessQueuedCommands() {
}
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing:
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
+ ActionStealing:
default:
if as.Type != ActionMoving || p.MoveTicks <= 0 {
p.ActionState = nil