aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-17 21:13:07 -0400
committerhistoria <[not public]>2026-06-17 21:13:07 -0400
commitec2e94e3463654a6288464a4c070b48ef9bf7368 (patch)
tree3c5df0f9a6a5968fa4c725a7d331825883ae798c /internal/game
parenteef5ddaa94f8cff6010d092c30fed53d2be01524 (diff)
downloadthehouseoficarus-ec2e94e3463654a6288464a4c070b48ef9bf7368.tar.gz
feat: smithing added, item/object interaction reworked. recipes and menus refactored.
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/action.go8
-rw-r--r--internal/game/action_cook.go153
-rw-r--r--internal/game/action_production.go467
-rw-r--r--internal/game/action_smelt.go153
-rw-r--r--internal/game/action_state.go9
-rw-r--r--internal/game/cmd_cook.go158
-rw-r--r--internal/game/cmd_look.go12
-rw-r--r--internal/game/cmd_smelt.go163
-rw-r--r--internal/game/cmd_smith.go342
-rw-r--r--internal/game/cmd_use.go69
-rw-r--r--internal/game/game.go17
-rw-r--r--internal/game/stations.go28
12 files changed, 943 insertions, 636 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
index 608528d..a62ccec 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -226,9 +226,13 @@ func (g *Game) AdvanceActions() {
case "search":
g.advanceSearch(sess, p)
case "cook":
- g.advanceCook(sess, p)
+ g.advanceProduction(sess, p)
case "smelt":
- g.advanceSmelt(sess, p)
+ g.advanceProduction(sess, p)
+ case "smith":
+ g.advanceProduction(sess, p)
+ case "combine":
+ g.advanceProduction(sess, p)
}
if p.Action == nil {
g.writePrompt(sess)
diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go
deleted file mode 100644
index a5bda41..0000000
--- a/internal/game/action_cook.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package game
-
-import (
- "fmt"
- "math/rand"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/engine"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) startCook(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) {
- g.CancelAction(p)
-
- if recipe.Skill != "" {
- skillLevel := p.Level(player.SkillName(recipe.Skill))
- if skillLevel < recipe.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d %s to cook that.", recipe.Level, recipe.Skill))
- return
- }
- }
-
- if !recipe.HasAllItems(p.HasItem) {
- sess.WriteLine("You don't have the required ingredients.")
- return
- }
-
- wait := recipe.Wait
- if wait <= 0 {
- wait = 6
- }
-
- p.Action = &action.Action{
- Type: "cook",
- TargetID: recipe.ID,
- TargetName: recipe.DisplayName(),
- Data: map[string]any{
- "recipe_id": recipe.ID,
- "station_name": stationName,
- "phase": 0,
- "wait": wait,
- },
- WaitLeft: engine.ToTicks(1),
- }
-
- p.ActionState = &ActionState{Type: ActionCooking, TargetName: recipe.DisplayName()}
-}
-
-func (g *Game) advanceCook(sess *net.Session, p *player.Player) bool {
- recipeID := p.Action.Data["recipe_id"].(string)
- stationName := p.Action.Data["station_name"].(string)
- phase := p.Action.Data["phase"].(int)
- wait := p.Action.Data["wait"].(float64)
-
- all, err := g.RecipeStore.LoadAll()
- if err != nil {
- g.CancelAction(p)
- return false
- }
- var recipe *action.RecipeDef
- for _, r := range all {
- if r.ID == recipeID {
- recipe = &r
- break
- }
- }
- if recipe == nil {
- g.CancelAction(p)
- return false
- }
-
- if phase == 0 {
- firstItem := recipe.FirstItemName(func(id string) (string, bool) {
- def, err := g.ItemStore.Load(id)
- if err != nil {
- return id, false
- }
- return def.Name, true
- })
- p.ActionState = &ActionState{Type: ActionCooking, TargetName: recipe.DisplayName()}
- if stationName != "" && stationName != "inventory" && stationName != "ground" {
- sess.WriteLine(fmt.Sprintf("\nYou start cooking %s on the %s.", firstItem, stationName))
- } else {
- sess.WriteLine(fmt.Sprintf("\nYou start making %s.", recipe.DisplayName()))
- }
- p.Action.Data["phase"] = 1
- p.Action.WaitLeft = engine.ToTicks(wait)
- return true
- }
-
- skillLevel := p.Level(player.SkillName(recipe.Skill))
- chance := 1.0
- if recipe.Success != nil {
- chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level)
- }
-
- if rand.Float64() < chance {
- outputID := recipe.Output
-
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- sess.WriteLine("Your inventory is too full!")
- g.CancelAction(p)
- return false
- }
-
- recipe.ConsumeAll(p.HasItem, p.RemoveItem)
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: outputID, Quantity: 1})
-
- if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill)))
- }
- }
- g.AccountStore.SaveCharacter(p)
-
- msg := recipe.Message
- if msg == "" {
- msg = fmt.Sprintf("Cooked to perfection. It looks great!")
- }
- sess.WriteLine(msg)
- if p.OptionBool("xp_drops") && recipe.XP > 0 {
- abbr := player.SkillAbbr[player.SkillName(recipe.Skill)]
- sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)))
- }
- } else {
- recipe.ConsumeAll(p.HasItem, p.RemoveItem)
-
- if recipe.Fail != "" {
- freeSlot := p.FirstFreeSlot()
- if freeSlot >= 0 {
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Fail, Quantity: 1})
- }
- }
- g.AccountStore.SaveCharacter(p)
-
- msg := recipe.FailMessage
- if msg == "" {
- msg = "You badly overcook it."
- }
- sess.WriteLine(msg)
- }
-
- if recipe.HasAllItems(p.HasItem) && p.FirstFreeSlot() >= 0 {
- p.Action.Data["phase"] = 1
- p.Action.WaitLeft = engine.ToTicks(wait)
- return true
- }
-
- g.CancelAction(p)
- return false
-}
diff --git a/internal/game/action_production.go b/internal/game/action_production.go
new file mode 100644
index 0000000..c0b76f3
--- /dev/null
+++ b/internal/game/action_production.go
@@ -0,0 +1,467 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+)
+
+type recipeEntry struct {
+ ItemName string
+ Recipe action.RecipeDef
+}
+
+func recipeMenuData(recipes []action.RecipeDef) []map[string]string {
+ out := make([]map[string]string, len(recipes))
+ for i, r := range recipes {
+ out[i] = map[string]string{"recipe_id": r.ID}
+ }
+ return out
+}
+
+func entryMenuData(entries []recipeEntry) []map[string]string {
+ out := make([]map[string]string, len(entries))
+ for i, e := range entries {
+ out[i] = map[string]string{"recipe_id": e.Recipe.ID}
+ }
+ return out
+}
+
+func (g *Game) promptHowMany(sess *net.Session, recipeID string) {
+ sess.PendingRecipeID = recipeID
+ sess.State = net.StateHowMany
+ sess.Write("How many (return for all)?: ")
+}
+
+func (g *Game) handleHowMany(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ recipeID := sess.PendingRecipeID
+ sess.PendingRecipeID = ""
+ sess.State = net.StateGame
+
+ input = strings.TrimSpace(input)
+
+ var count int
+ if input == "" {
+ count = 0
+ } else if n, err := strconv.Atoi(input); err == nil && n > 0 {
+ count = n
+ } else {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+
+ var recipe *action.RecipeDef
+ if strings.HasPrefix(recipeID, "combine_") {
+ itemID := strings.TrimPrefix(recipeID, "combine_")
+ itemDef, err := g.ItemStore.Load(itemID)
+ if err != nil || len(itemDef.MadeFrom) == 0 {
+ g.reprompt(sess)
+ return
+ }
+ recipe = g.buildCombineRecipe(itemDef)
+ } else {
+ all, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.reprompt(sess)
+ return
+ }
+ for _, r := range all {
+ if r.ID == recipeID {
+ recipe = &r
+ break
+ }
+ }
+ }
+
+ if recipe == nil {
+ g.reprompt(sess)
+ return
+ }
+
+ g.startProductionFromRecipe(sess, p, recipe, count)
+}
+
+func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef {
+ var consume []action.ConsumeEntry
+ for _, mf := range def.MadeFrom {
+ consume = append(consume, action.ConsumeEntry{
+ Items: mf.Items,
+ Qty: mf.Qty,
+ Byproducts: mf.Byproducts,
+ })
+ }
+ wait := def.Ticks
+ if wait <= 0 {
+ wait = 2
+ }
+ return &action.RecipeDef{
+ ID: "combine_" + def.ID,
+ Type: "combine",
+ Wait: wait,
+ Consume: consume,
+ Output: def.ID,
+ Message: fmt.Sprintf("You create %s.", def.Name),
+ }
+}
+
+func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, actionType, displayVerb, startMsg, endMsg string, count int) {
+ g.CancelAction(p)
+
+ if recipe.Skill != "" {
+ skillLevel := p.Level(player.SkillName(recipe.Skill))
+ if skillLevel < recipe.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", recipe.Level, recipe.Skill))
+ return
+ }
+ }
+
+ if !recipe.HasAllItemsQty(p.CountItem) {
+ sess.WriteLine("You don't have the required materials.")
+ return
+ }
+
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+
+ canPlace := false
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ if outDef != nil && outDef.Stackable {
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == recipe.Output {
+ canPlace = true
+ break
+ }
+ }
+ }
+ if !canPlace && p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ return
+ }
+
+ wait := recipe.Wait
+ if wait <= 0 {
+ wait = 4
+ }
+
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+
+ p.Action = &action.Action{
+ Type: actionType,
+ TargetID: recipe.ID,
+ TargetName: outputName,
+ Data: map[string]any{
+ "recipe_id": recipe.ID,
+ "phase": 0,
+ "wait": wait,
+ "start_msg": startMsg,
+ "end_msg": endMsg,
+ "remaining": count,
+ },
+ WaitLeft: engine.ToTicks(1),
+ }
+
+ p.ActionState = &ActionState{Type: ActionProducing, TargetName: outputName, Verb: displayVerb}
+}
+
+func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
+ _, stationName := g.findStation(p.RoomID, recipe.Station)
+ if stationName == "" {
+ stationName = "inventory"
+ }
+
+ firstItem := recipe.FirstItemName(func(id string) (string, bool) {
+ def, err := g.ItemStore.Load(id)
+ if err != nil {
+ return id, false
+ }
+ return def.Name, true
+ })
+
+ var actionType, displayVerb, startMsg, endMsg string
+ switch recipe.Type {
+ case "cooking":
+ actionType = "cook"
+ displayVerb = "cooking"
+ if stationName != "inventory" {
+ startMsg = fmt.Sprintf("You start cooking %s on the %s.", firstItem, stationName)
+ } else {
+ startMsg = fmt.Sprintf("You start making %s.", firstItem)
+ }
+ endMsg = "You've cooked everything you can."
+ case "smelting":
+ actionType = "smelt"
+ displayVerb = "smelting"
+ startMsg = fmt.Sprintf("You begin to process %s in the furnace.", firstItem)
+ endMsg = "You've processed all the ore in your inventory."
+ case "smithing":
+ actionType = "smith"
+ displayVerb = "smithing"
+ startMsg = fmt.Sprintf("You hammer the %s on the anvil...", firstItem)
+ endMsg = "You've used all the bars you can."
+ case "combine":
+ actionType = "combine"
+ displayVerb = "combining"
+ startMsg = fmt.Sprintf("You start combining %s.", firstItem)
+ endMsg = "You've run out of materials."
+ default:
+ actionType = "craft"
+ displayVerb = "crafting"
+ startMsg = fmt.Sprintf("You start crafting with %s.", firstItem)
+ endMsg = "You've run out of materials."
+ }
+
+ g.startProduction(sess, p, recipe, actionType, displayVerb, startMsg, endMsg, count)
+}
+
+func (g *Game) loadProductionRecipe(recipeID string) *action.RecipeDef {
+ all, err := g.RecipeStore.LoadAll()
+ if err == nil {
+ for _, r := range all {
+ if r.ID == recipeID {
+ return &r
+ }
+ }
+ }
+ if strings.HasPrefix(recipeID, "combine_") {
+ itemID := strings.TrimPrefix(recipeID, "combine_")
+ if itemDef, err := g.ItemStore.Load(itemID); err == nil && len(itemDef.MadeFrom) > 0 {
+ return g.buildCombineRecipe(itemDef)
+ }
+ }
+ return nil
+}
+
+func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
+ recipeID := p.Action.Data["recipe_id"].(string)
+ phase := p.Action.Data["phase"].(int)
+ wait := p.Action.Data["wait"].(float64)
+ startMsg, _ := p.Action.Data["start_msg"].(string)
+ endMsg, _ := p.Action.Data["end_msg"].(string)
+ remaining, _ := p.Action.Data["remaining"].(int)
+
+ recipe := g.loadProductionRecipe(recipeID)
+ if recipe == nil {
+ g.CancelAction(p)
+ return false
+ }
+
+ if phase == 0 {
+ if startMsg != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", startMsg))
+ }
+ p.Action.Data["phase"] = 1
+ p.Action.WaitLeft = engine.ToTicks(wait)
+ return true
+ }
+
+ skillLevel := p.Level(player.SkillName(recipe.Skill))
+ chance := 1.0
+ if recipe.Success != nil {
+ chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level)
+ }
+
+ if rand.Float64() < chance {
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+
+ byproducts := g.collectByproducts(p, recipe)
+
+ placed := false
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ if outDef != nil && outDef.Stackable {
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == recipe.Output {
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+ slot.Quantity += outputQty
+ placed = true
+ break
+ }
+ }
+ }
+ if !placed {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ g.CancelAction(p)
+ return false
+ }
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
+ }
+
+ for _, bp := range byproducts {
+ if slot := p.FirstFreeSlot(); slot >= 0 {
+ p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1})
+ }
+ }
+
+ if recipe.XP > 0 {
+ if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill)))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ msg := recipe.Message
+ if msg == "" {
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+ msg = fmt.Sprintf("You produce %s.", outputName)
+ }
+ if p.OptionBool("xp_drops") && recipe.XP > 0 {
+ abbr := player.SkillAbbr[player.SkillName(recipe.Skill)]
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr))
+ }
+ sess.WriteLine(msg)
+ } else {
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+
+ if recipe.Fail != "" {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot >= 0 {
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Fail, Quantity: 1})
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ msg := recipe.FailMessage
+ if msg == "" {
+ msg = "You fail and the materials are lost."
+ }
+ sess.WriteLine(g.colorize(sess, "damage", msg))
+ }
+
+ if remaining > 0 {
+ remaining--
+ p.Action.Data["remaining"] = remaining
+ if remaining <= 0 {
+ if endMsg != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", endMsg))
+ }
+ g.CancelAction(p)
+ return false
+ }
+ }
+
+ if g.canContinueProduction(p, recipe) {
+ p.Action.WaitLeft = engine.ToTicks(wait)
+ return true
+ }
+
+ if endMsg != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", endMsg))
+ }
+ g.CancelAction(p)
+ return false
+}
+
+func (g *Game) collectByproducts(p *player.Player, recipe *action.RecipeDef) []string {
+ var byproducts []string
+ for _, e := range recipe.Consume {
+ if len(e.Byproducts) == 0 {
+ continue
+ }
+ for i, id := range e.Items {
+ if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" {
+ byproducts = append(byproducts, e.Byproducts[i])
+ break
+ }
+ }
+ }
+ return byproducts
+}
+
+func (g *Game) canContinueProduction(p *player.Player, recipe *action.RecipeDef) bool {
+ if !recipe.HasAllItemsQty(p.CountItem) {
+ return false
+ }
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ if outDef != nil && outDef.Stackable {
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot != nil && slot.ItemID == recipe.Output {
+ return true
+ }
+ }
+ }
+ return p.FirstFreeSlot() >= 0
+}
+
+func (g *Game) handleRecipeChoice(sess *net.Session, input string) {
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return
+ }
+
+ choice, err := strconv.Atoi(input)
+ if err != nil {
+ sess.State = net.StateGame
+ sess.PendingMenu = nil
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+
+ if len(sess.PendingMenu) > 0 {
+ menuData := sess.PendingMenu
+ sess.PendingMenu = nil
+ if choice <= 0 || choice > len(menuData)+1 {
+ sess.WriteLine("Invalid choice.")
+ return
+ }
+ sess.State = net.StateGame
+ if choice == len(menuData)+1 {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+ entry := menuData[choice-1]
+
+ if cid, ok := entry["combine_item"]; ok {
+ g.promptHowMany(sess, "combine_"+cid)
+ return
+ }
+
+ if barID, ok := entry["bar_id"]; ok {
+ p := sess.Player.(*player.Player)
+ allRecipes, _ := g.RecipeStore.LoadAll()
+ g.showSmithTable(sess, p, barID, allRecipes)
+ return
+ }
+
+ if rid, ok := entry["recipe_id"]; ok {
+ g.promptHowMany(sess, rid)
+ return
+ }
+
+ g.reprompt(sess)
+ return
+ }
+
+ sess.State = net.StateGame
+ g.reprompt(sess)
+}
diff --git a/internal/game/action_smelt.go b/internal/game/action_smelt.go
deleted file mode 100644
index 75fb8a0..0000000
--- a/internal/game/action_smelt.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package game
-
-import (
- "fmt"
- "math/rand"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/engine"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) startSmelt(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) {
- g.CancelAction(p)
-
- if recipe.Skill != "" {
- skillLevel := p.Level(player.SkillName(recipe.Skill))
- if skillLevel < recipe.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", recipe.Level, recipe.Skill))
- return
- }
- }
-
- if !recipe.HasAllItemsQty(p.CountItem) {
- sess.WriteLine("You don't have the required ores.")
- return
- }
-
- if p.FirstFreeSlot() == -1 {
- sess.WriteLine("Your inventory is too full!")
- return
- }
-
- wait := recipe.Wait
- if wait <= 0 {
- wait = 4
- }
-
- outputName := recipe.Output
- if def, err := g.ItemStore.Load(recipe.Output); err == nil {
- outputName = def.Name
- }
-
- p.Action = &action.Action{
- Type: "smelt",
- TargetID: recipe.ID,
- TargetName: recipe.DisplayName(),
- Data: map[string]any{
- "recipe_id": recipe.ID,
- "station_name": stationName,
- "phase": 0,
- "wait": wait,
- },
- WaitLeft: engine.ToTicks(1),
- }
-
- p.ActionState = &ActionState{Type: ActionSmelting, TargetName: outputName}
-}
-
-func (g *Game) advanceSmelt(sess *net.Session, p *player.Player) bool {
- recipeID := p.Action.Data["recipe_id"].(string)
- phase := p.Action.Data["phase"].(int)
- wait := p.Action.Data["wait"].(float64)
-
- all, err := g.RecipeStore.LoadAll()
- if err != nil {
- g.CancelAction(p)
- return false
- }
- var recipe *action.RecipeDef
- for _, r := range all {
- if r.ID == recipeID {
- recipe = &r
- break
- }
- }
- if recipe == nil {
- g.CancelAction(p)
- return false
- }
-
- if phase == 0 {
- firstItem := recipe.FirstItemName(func(id string) (string, bool) {
- def, err := g.ItemStore.Load(id)
- if err != nil {
- return id, false
- }
- return def.Name, true
- })
- sess.WriteLine(fmt.Sprintf("\nYou begin to process %s in the furnace.", firstItem))
- p.Action.Data["phase"] = 1
- p.Action.WaitLeft = engine.ToTicks(wait)
- return true
- }
-
- skillLevel := p.Level(player.SkillName(recipe.Skill))
- chance := 1.0
- if recipe.Success != nil {
- chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level)
- }
-
- if rand.Float64() < chance {
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- sess.WriteLine("Your inventory is too full!")
- g.CancelAction(p)
- return false
- }
-
- recipe.ConsumeAll(p.HasItem, p.RemoveItem)
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: 1})
-
- if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill)))
- }
- }
- g.AccountStore.SaveCharacter(p)
-
- msg := recipe.Message
- if msg == "" {
- outputName := recipe.Output
- if def, loadErr := g.ItemStore.Load(recipe.Output); loadErr == nil {
- outputName = def.Name
- }
- msg = fmt.Sprintf("You remove a white hot %s!", outputName)
- }
- if p.OptionBool("xp_drops") && recipe.XP > 0 {
- abbr := player.SkillAbbr[player.SkillName(recipe.Skill)]
- msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr))
- }
- sess.WriteLine(msg)
- } else {
- recipe.ConsumeAll(p.HasItem, p.RemoveItem)
- g.AccountStore.SaveCharacter(p)
-
- msg := recipe.FailMessage
- if msg == "" {
- msg = "You fail to smelt a usable bar."
- }
- sess.WriteLine(g.colorize(sess, "damage", msg))
- }
-
- if recipe.HasAllItemsQty(p.CountItem) && p.FirstFreeSlot() >= 0 {
- p.Action.Data["phase"] = 0
- p.Action.WaitLeft = engine.ToTicks(wait)
- return true
- }
-
- sess.WriteLine("\nYou've processed all the ore in your inventory.")
- g.CancelAction(p)
- return false
-}
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 6ce3734..26c22f3 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -19,8 +19,7 @@ const (
ActionSearching ActionType = "searching"
ActionResting ActionType = "resting"
ActionWalking ActionType = "walking"
- ActionCooking ActionType = "cooking"
- ActionSmelting ActionType = "smelting"
+ ActionProducing ActionType = "producing"
ActionEating ActionType = "eating"
)
@@ -67,10 +66,8 @@ func (a *ActionState) Description() string {
return "resting"
case ActionWalking:
return "walking somewhere with a purpose!"
- case ActionCooking:
- return "cooking some " + a.TargetName
- case ActionSmelting:
- return "smelting some " + a.TargetName
+ case ActionProducing:
+ return a.Verb + " some " + a.TargetName
case ActionEating:
return "eating " + a.TargetName
}
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
index 054a4b0..ce33ccd 100644
--- a/internal/game/cmd_cook.go
+++ b/internal/game/cmd_cook.go
@@ -2,25 +2,18 @@ package game
import (
"fmt"
- "strconv"
- "strings"
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
-type cookableEntry struct {
- ItemName string
- Recipe action.RecipeDef
-}
-
func (g *Game) doCook(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
- stationName := g.findCookingStation(p.RoomID)
- if stationName == "" {
+ stationDefID, stationName := g.findStation(p.RoomID, []string{"fire", "cooking_range"})
+ if stationDefID == "" {
sess.WriteLine("You need a fire or cooking range to cook.")
return
}
@@ -32,7 +25,7 @@ func (g *Game) doCook(sess *net.Session, input string) {
}
if input == "" {
- g.showCookMenu(sess, p, allRecipes, stationName)
+ g.showCookMenu(sess, p, allRecipes, stationDefID, stationName)
return
}
@@ -61,24 +54,23 @@ func (g *Game) doCook(sess *net.Session, input string) {
if r.Type != "cooking" {
continue
}
- if stationOK(stationName, r.Station) && r.MatchesEntry(itemID) && r.HasAllItems(p.HasItem) {
+ if stationMatch(stationDefID, r.Station) && r.MatchesEntry(itemID) && r.HasAllItems(p.HasItem) {
recipes = append(recipes, r)
}
}
if len(recipes) == 0 {
- sess.WriteLine("You can't cook that here.")
+ sess.WriteLine("You can't cook that.")
return
}
if len(recipes) == 1 {
- g.startCook(sess, p, &recipes[0], stationName)
+ g.promptHowMany(sess, recipes[0].ID)
return
}
- sess.PendingRecipeItem = itemID
- sess.PendingCookMenu = recipesToMenuData(recipes)
- sess.State = net.StateCookRecipe
+ sess.PendingMenu = recipeMenuData(recipes)
+ sess.State = net.StateRecipeChoice
sess.WriteLine(fmt.Sprintf("\nWhat would you like to cook on the %s?", stationName))
for i, r := range recipes {
sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
@@ -86,15 +78,15 @@ func (g *Game) doCook(sess *net.Session, input string) {
sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1))
}
-func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) {
+func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) {
seen := make(map[string]bool)
- var cookables []cookableEntry
+ var entries []recipeEntry
for _, r := range allRecipes {
if r.Type != "cooking" {
continue
}
- if !stationOK(stationName, r.Station) {
+ if !stationMatch(stationDefID, r.Station) {
continue
}
if seen[r.ID] {
@@ -104,28 +96,26 @@ func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []ac
continue
}
seen[r.ID] = true
- cookables = append(cookables, cookableEntry{g.recipeName(sess, &r), r})
+ entries = append(entries, recipeEntry{g.recipeName(sess, &r), r})
}
- if len(cookables) == 0 {
+ if len(entries) == 0 {
sess.WriteLine("You don't have anything you can cook.")
return
}
- if len(cookables) == 1 {
- g.startCook(sess, p, &cookables[0].Recipe, stationName)
+ if len(entries) == 1 {
+ g.promptHowMany(sess, entries[0].Recipe.ID)
return
}
- sess.PendingRecipeItem = ""
- sess.State = net.StateCookRecipe
+ sess.State = net.StateRecipeChoice
sess.WriteLine(fmt.Sprintf("\nWhat would you like to cook on the %s?", stationName))
- for i, c := range cookables {
- sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, c.ItemName))
+ for i, e := range entries {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, e.ItemName))
}
- sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(cookables)+1))
-
- sess.PendingCookMenu = makeCookMenuData(cookables)
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(entries)+1))
+ sess.PendingMenu = entryMenuData(entries)
}
func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string {
@@ -136,111 +126,3 @@ func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string {
}
return r.DisplayName()
}
-
-func makeCookMenuData(cookables []cookableEntry) []map[string]string {
- out := make([]map[string]string, len(cookables))
- for i, c := range cookables {
- out[i] = map[string]string{"recipe_id": c.Recipe.ID}
- }
- return out
-}
-
-func recipesToMenuData(recipes []action.RecipeDef) []map[string]string {
- out := make([]map[string]string, len(recipes))
- for i, r := range recipes {
- out[i] = map[string]string{"recipe_id": r.ID}
- }
- return out
-}
-
-func stationOK(stationName string, recipeStations []string) bool {
- if len(recipeStations) == 0 {
- return false
- }
- for _, rs := range recipeStations {
- if (rs == "fire" && stationName == "fire") || (rs == "cooking_range" && stationName == "cooking range") {
- return true
- }
- }
- return false
-}
-
-func (g *Game) findCookingStation(roomID int) string {
- for _, obj := range g.World.AllObjInstances(roomID) {
- if obj.Depleted {
- continue
- }
- if obj.DefID == "fire" {
- return "fire"
- }
- if obj.DefID == "cooking_range" {
- return "cooking range"
- }
- }
- return ""
-}
-
-func (g *Game) handleCookRecipe(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- input = strings.TrimSpace(input)
- if input == "" {
- return
- }
-
- choice, err := strconv.Atoi(input)
- if err != nil {
- sess.State = net.StateGame
- sess.PendingRecipeItem = ""
- sess.PendingCookMenu = nil
- sess.WriteLine("You decide not to cook anything.")
- g.reprompt(sess)
- return
- }
-
- if len(sess.PendingCookMenu) > 0 {
- menuData := sess.PendingCookMenu
- sess.PendingCookMenu = nil
- if choice <= 0 || choice > len(menuData)+1 {
- sess.WriteLine("Invalid choice.")
- return
- }
- sess.State = net.StateGame
- if choice == len(menuData)+1 {
- sess.WriteLine("You decide not to cook anything.")
- g.reprompt(sess)
- return
- }
- entry := menuData[choice-1]
-
- if cid, ok := entry["combine_item"]; ok {
- def, err := g.ItemStore.Load(cid)
- if err == nil {
- p := sess.Player.(*player.Player)
- g.produceCombined(sess, p, def)
- }
- g.reprompt(sess)
- return
- }
-
- all, err := g.RecipeStore.LoadAll()
- if err != nil {
- g.reprompt(sess)
- return
- }
- for _, r := range all {
- if r.ID == entry["recipe_id"] {
- stationName := "inventory"
- if len(r.Station) > 0 {
- stationName = g.findCookingStation(p.RoomID)
- }
- g.startCook(sess, p, &r, stationName)
- return
- }
- }
- g.reprompt(sess)
- return
- }
-
- sess.State = net.StateGame
- g.reprompt(sess)
-}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index 77c8776..9809e9b 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -469,8 +469,8 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
sess.WriteLine(def.Name)
}
- if desc, ok := def.Props["description"].(string); ok && desc != "" {
- sess.WriteLine(fmt.Sprintf(" %s", desc))
+ if def.Description != "" && !strings.Contains(def.Description, "{quality}") {
+ sess.WriteLine(fmt.Sprintf(" %s", def.Description))
}
if p.OptionBool("depletion") {
@@ -491,11 +491,9 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
}
}
for _, ist := range instances {
- if ist.Quality > 0 {
- if qdesc, ok := def.Props["quality_description"].(string); ok && qdesc != "" {
- qdesc = strings.ReplaceAll(qdesc, "{quality}", fmt.Sprintf("%.0f", ist.Quality))
- sess.WriteLine(fmt.Sprintf(" %s", qdesc))
- }
+ if ist.Quality > 0 && strings.Contains(def.Description, "{quality}") {
+ desc := strings.ReplaceAll(def.Description, "{quality}", fmt.Sprintf("%.0f", ist.Quality))
+ sess.WriteLine(fmt.Sprintf(" %s", desc))
}
}
return
diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go
index a308352..e0889f6 100644
--- a/internal/game/cmd_smelt.go
+++ b/internal/game/cmd_smelt.go
@@ -2,25 +2,18 @@ package game
import (
"fmt"
- "strconv"
- "strings"
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
-type smeltableEntry struct {
- ItemName string
- Recipe action.RecipeDef
-}
-
func (g *Game) doSmelt(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
- stationName := g.findFurnace(p.RoomID)
- if stationName == "" {
+ stationDefID, stationName := g.findStation(p.RoomID, []string{"furnace"})
+ if stationDefID == "" {
sess.WriteLine("You need a furnace to smelt.")
return
}
@@ -32,7 +25,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
}
if input == "" {
- g.showSmeltMenu(sess, p, allRecipes, stationName)
+ g.showSmeltMenu(sess, p, allRecipes, stationDefID, stationName)
return
}
@@ -55,38 +48,27 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
var recipes []action.RecipeDef
for _, r := range allRecipes {
- if r.Type != "smelting" {
- continue
- }
- if !smeltStationOK(stationName, r.Station) {
+ if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) || !r.MatchesEntry(itemID) {
continue
}
- if !r.MatchesEntry(itemID) {
- continue
- }
- if !r.HasAllItemsQty(p.CountItem) {
- continue
- }
- skillLevel := p.Level(player.SkillName(r.Skill))
- if skillLevel < r.Level {
+ if !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level {
continue
}
recipes = append(recipes, r)
}
if len(recipes) == 0 {
- sess.WriteLine("You can't smelt that here.")
+ sess.WriteLine("You can't smelt that.")
return
}
if len(recipes) == 1 {
- g.startSmelt(sess, p, &recipes[0], stationName)
+ g.promptHowMany(sess, recipes[0].ID)
return
}
- sess.PendingRecipeItem = itemID
- sess.PendingCookMenu = smeltRecipesToMenuData(recipes)
- sess.State = net.StateSmeltRecipe
+ sess.PendingMenu = recipeMenuData(recipes)
+ sess.State = net.StateRecipeChoice
sess.WriteLine("\nWhat would you like to smelt?")
for i, r := range recipes {
sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
@@ -94,137 +76,36 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1))
}
-func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) {
+func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) {
seen := make(map[string]bool)
- var smeltables []smeltableEntry
+ var entries []recipeEntry
for _, r := range allRecipes {
- if r.Type != "smelting" {
- continue
- }
- if !smeltStationOK(stationName, r.Station) {
- continue
- }
- if seen[r.ID] {
- continue
- }
- if !r.HasAllItemsQty(p.CountItem) {
+ if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) {
continue
}
- skillLevel := p.Level(player.SkillName(r.Skill))
- if skillLevel < r.Level {
+ if seen[r.ID] || !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level {
continue
}
seen[r.ID] = true
- smeltables = append(smeltables, smeltableEntry{g.recipeName(sess, &r), r})
+ entries = append(entries, recipeEntry{g.recipeName(sess, &r), r})
}
- if len(smeltables) == 0 {
+ if len(entries) == 0 {
sess.WriteLine("You don't have anything you can smelt.")
return
}
- if len(smeltables) == 1 {
- g.startSmelt(sess, p, &smeltables[0].Recipe, stationName)
+ if len(entries) == 1 {
+ g.promptHowMany(sess, entries[0].Recipe.ID)
return
}
- sess.PendingRecipeItem = ""
- sess.State = net.StateSmeltRecipe
+ sess.State = net.StateRecipeChoice
sess.WriteLine("\nWhat would you like to smelt?")
- for i, s := range smeltables {
- sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, s.ItemName))
- }
- sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(smeltables)+1))
-
- sess.PendingCookMenu = makeSmeltMenuData(smeltables)
-}
-
-func (g *Game) handleSmeltRecipe(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- input = strings.TrimSpace(input)
- if input == "" {
- return
- }
-
- choice, err := strconv.Atoi(input)
- if err != nil {
- sess.State = net.StateGame
- sess.PendingRecipeItem = ""
- sess.PendingCookMenu = nil
- sess.WriteLine("You decide not to smelt anything.")
- g.reprompt(sess)
- return
- }
-
- if len(sess.PendingCookMenu) > 0 {
- menuData := sess.PendingCookMenu
- sess.PendingCookMenu = nil
- if choice <= 0 || choice > len(menuData)+1 {
- sess.WriteLine("Invalid choice.")
- return
- }
- sess.State = net.StateGame
- if choice == len(menuData)+1 {
- sess.WriteLine("You decide not to smelt anything.")
- g.reprompt(sess)
- return
- }
- entry := menuData[choice-1]
-
- all, loadErr := g.RecipeStore.LoadAll()
- if loadErr != nil {
- g.reprompt(sess)
- return
- }
- for _, r := range all {
- if r.ID == entry["recipe_id"] {
- stationName := g.findFurnace(p.RoomID)
- g.startSmelt(sess, p, &r, stationName)
- return
- }
- }
- g.reprompt(sess)
- return
- }
-
- sess.State = net.StateGame
- g.reprompt(sess)
-}
-
-func smeltStationOK(stationName string, recipeStations []string) bool {
- for _, rs := range recipeStations {
- if rs == "furnace" && stationName == "furnace" {
- return true
- }
- }
- return false
-}
-
-func (g *Game) findFurnace(roomID int) string {
- for _, obj := range g.World.AllObjInstances(roomID) {
- if obj.Depleted {
- continue
- }
- if obj.DefID == "furnace" {
- return "furnace"
- }
- }
- return ""
-}
-
-func makeSmeltMenuData(smeltables []smeltableEntry) []map[string]string {
- out := make([]map[string]string, len(smeltables))
- for i, s := range smeltables {
- out[i] = map[string]string{"recipe_id": s.Recipe.ID}
- }
- return out
-}
-
-func smeltRecipesToMenuData(recipes []action.RecipeDef) []map[string]string {
- out := make([]map[string]string, len(recipes))
- for i, r := range recipes {
- out[i] = map[string]string{"recipe_id": r.ID}
+ for i, e := range entries {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, e.ItemName))
}
- return out
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(entries)+1))
+ sess.PendingMenu = entryMenuData(entries)
}
diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go
new file mode 100644
index 0000000..30dcddc
--- /dev/null
+++ b/internal/game/cmd_smith.go
@@ -0,0 +1,342 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doSmith(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ g.CancelAction(p)
+
+ stationDefID, _ := g.findStation(p.RoomID, []string{"anvil"})
+ if stationDefID == "" {
+ sess.WriteLine("You need an anvil to smith.")
+ return
+ }
+
+ if !g.hasToolType(p, "hammer") {
+ sess.WriteLine("You need a hammer to smith.")
+ return
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ if input != "" {
+ matches := g.findInventoryMatches(input, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input))
+ return
+ }
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+ barID := matches[0].ID
+ if !hasSmithingRecipes(allRecipes, barID) {
+ sess.WriteLine("You can't smith that.")
+ return
+ }
+ g.showSmithTable(sess, p, barID, allRecipes)
+ return
+ }
+
+ barTypes := findSmithableBars(allRecipes, p)
+ if len(barTypes) == 0 {
+ sess.WriteLine("You don't have any bars to smith.")
+ return
+ }
+
+ if len(barTypes) == 1 {
+ g.showSmithTable(sess, p, barTypes[0], allRecipes)
+ return
+ }
+
+ sess.PendingMenu = barMenuData(barTypes)
+ sess.State = net.StateRecipeChoice
+ sess.WriteLine("\nWhich bars would you like to smith?")
+ for i, barID := range barTypes {
+ def, _ := g.ItemStore.Load(barID)
+ name := barID
+ if def != nil {
+ name = g.itemColorize(sess, def, def.Name)
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, name))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(barTypes)+1))
+}
+
+func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string, allRecipes []action.RecipeDef) {
+ barDef, _ := g.ItemStore.Load(barID)
+ barName := barID
+ if barDef != nil {
+ barName = barDef.Name
+ }
+
+ var recipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type == "smithing" && r.MatchesEntry(barID) {
+ recipes = append(recipes, r)
+ }
+ }
+
+ if len(recipes) == 0 {
+ sess.WriteLine("There's nothing to smith with that.")
+ return
+ }
+
+ sort.Slice(recipes, func(i, j int) bool {
+ return recipes[i].Level < recipes[j].Level
+ })
+
+ mode := g.colorMode(sess)
+ skillLevel := p.Level(player.Smithing)
+ barCount := p.CountItem(barID)
+ dimSpec := color.Parse("240")
+
+ tbl := &Table{
+ Title: titleCase(barName) + " Smithing",
+ Columns: []string{"Product", "Inputs", "Level Req."},
+ }
+
+ for _, r := range recipes {
+ outDef, _ := g.ItemStore.Load(r.Output)
+ productName := r.Output
+ if outDef != nil {
+ productName = outDef.Name
+ }
+
+ bars := barsRequired(r, barID)
+ inputStr := "1 bar"
+ if bars > 1 {
+ inputStr = fmt.Sprintf("%d bars", bars)
+ }
+ levelStr := fmt.Sprint(r.Level)
+
+ canMake := barCount >= bars && skillLevel >= r.Level
+ if canMake {
+ productName = g.itemColorize(sess, outDef, productName)
+ } else {
+ productName = color.Render(mode, dimSpec, productName)
+ inputStr = color.Render(mode, dimSpec, inputStr)
+ levelStr = color.Render(mode, dimSpec, levelStr)
+ }
+
+ tbl.Rows = append(tbl.Rows, []string{productName, inputStr, levelStr})
+ }
+
+ unicode := p.OptionBool("unicode")
+ sess.WriteLine("")
+ for _, line := range tbl.Render(unicode) {
+ sess.WriteLine(line)
+ }
+
+ lastFlag := fmt.Sprintf("last_smith_%s", barID)
+ lastRecipeID, _ := p.Flags[lastFlag].(string)
+ hint := ""
+ if lastRecipeID != "" {
+ for _, r := range recipes {
+ if r.ID == lastRecipeID {
+ if outDef, err := g.ItemStore.Load(r.Output); err == nil {
+ hint = outDef.Name
+ }
+ break
+ }
+ }
+ }
+
+ if hint != "" {
+ sess.Write(fmt.Sprintf("Produce what (enter for all %s): ", hint))
+ } else {
+ sess.Write("Produce what: ")
+ }
+
+ sess.PendingRecipeID = barID
+ sess.State = net.StateSmithProduct
+}
+
+func (g *Game) handleSmithProduct(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ barID := sess.PendingRecipeID
+ sess.PendingRecipeID = ""
+ sess.State = net.StateGame
+
+ input = strings.TrimSpace(input)
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.reprompt(sess)
+ return
+ }
+ var recipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type == "smithing" && r.MatchesEntry(barID) {
+ recipes = append(recipes, r)
+ }
+ }
+
+ var targetRecipe *action.RecipeDef
+ var count int
+
+ if input == "" {
+ lastFlag := fmt.Sprintf("last_smith_%s", barID)
+ lastRecipeID, _ := p.Flags[lastFlag].(string)
+ if lastRecipeID == "" {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+ for i := range recipes {
+ if recipes[i].ID == lastRecipeID {
+ targetRecipe = &recipes[i]
+ break
+ }
+ }
+ if targetRecipe == nil {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+ count = 0
+ } else {
+ qty, productName := parseQty(input)
+ count = qty
+ productName = strings.ToLower(strings.TrimSpace(productName))
+
+ for i := range recipes {
+ outDef, _ := g.ItemStore.Load(recipes[i].Output)
+ name := recipes[i].Output
+ if outDef != nil {
+ name = outDef.Name
+ }
+ lower := strings.ToLower(name)
+ if lower == productName || strings.HasPrefix(lower, productName) {
+ targetRecipe = &recipes[i]
+ break
+ }
+ }
+
+ if targetRecipe == nil {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+ }
+
+ skillLevel := p.Level(player.Smithing)
+ if skillLevel < targetRecipe.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d smithing to make that.", targetRecipe.Level))
+ g.reprompt(sess)
+ return
+ }
+ if !targetRecipe.HasAllItemsQty(p.CountItem) {
+ sess.WriteLine("You don't have enough bars.")
+ g.reprompt(sess)
+ return
+ }
+
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ p.Flags[fmt.Sprintf("last_smith_%s", barID)] = targetRecipe.ID
+ g.AccountStore.SaveCharacter(p)
+
+ g.startProductionFromRecipe(sess, p, targetRecipe, count)
+}
+
+func (g *Game) hasToolType(p *player.Player, toolType string) bool {
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ if def, err := g.ItemStore.Load(itemID); err == nil && def.ToolType == toolType {
+ return true
+ }
+ }
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot == nil {
+ continue
+ }
+ if def, err := g.ItemStore.Load(slot.ItemID); err == nil && def.ToolType == toolType {
+ return true
+ }
+ }
+ return false
+}
+
+func hasSmithingRecipes(allRecipes []action.RecipeDef, barID string) bool {
+ for _, r := range allRecipes {
+ if r.Type == "smithing" && r.MatchesEntry(barID) {
+ return true
+ }
+ }
+ return false
+}
+
+func findSmithableBars(allRecipes []action.RecipeDef, p *player.Player) []string {
+ barSet := make(map[string]bool)
+ for _, r := range allRecipes {
+ if r.Type != "smithing" {
+ continue
+ }
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if p.HasItem(id) {
+ barSet[id] = true
+ }
+ }
+ }
+ }
+ var bars []string
+ for id := range barSet {
+ bars = append(bars, id)
+ }
+ sort.Strings(bars)
+ return bars
+}
+
+func barsRequired(r action.RecipeDef, barID string) int {
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if id == barID {
+ qty := e.Qty
+ if qty <= 0 {
+ qty = 1
+ }
+ return qty
+ }
+ }
+ }
+ return 1
+}
+
+func barMenuData(barIDs []string) []map[string]string {
+ out := make([]map[string]string, len(barIDs))
+ for i, id := range barIDs {
+ out[i] = map[string]string{"bar_id": id}
+ }
+ return out
+}
+
+func titleCase(s string) string {
+ words := strings.Fields(s)
+ for i, w := range words {
+ if len(w) > 0 {
+ words[i] = strings.ToUpper(w[:1]) + w[1:]
+ }
+ }
+ return strings.Join(words, " ")
+}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 247df72..cb521f3 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -10,6 +10,11 @@ import (
)
func (g *Game) doUse(sess *net.Session, input string) {
+ if strings.TrimSpace(input) == "" {
+ g.doHelp(sess, "use")
+ return
+ }
+
p := sess.Player.(*player.Player)
g.CancelAction(p)
@@ -94,12 +99,11 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
return
}
if len(filtered) == 1 {
- g.startSmelt(sess, p, &filtered[0], def.Name)
+ g.promptHowMany(sess, filtered[0].ID)
return
}
- sess.PendingRecipeItem = itemAID
- sess.PendingCookMenu = smeltRecipesToMenuData(filtered)
- sess.State = net.StateSmeltRecipe
+ sess.PendingMenu = recipeMenuData(filtered)
+ sess.State = net.StateRecipeChoice
sess.WriteLine("\nWhat would you like to smelt?")
for i, r := range filtered {
sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
@@ -107,13 +111,21 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(filtered)+1))
return
}
+ if recipes[0].Type == "smithing" {
+ if !g.hasToolType(p, "hammer") {
+ sess.WriteLine("You need a hammer to smith.")
+ return
+ }
+ allRecipes, _ := g.RecipeStore.LoadAll()
+ g.showSmithTable(sess, p, itemAID, allRecipes)
+ return
+ }
if len(recipes) == 1 {
- g.startCook(sess, p, &recipes[0], def.Name)
+ g.promptHowMany(sess, recipes[0].ID)
return
}
- sess.PendingRecipeItem = itemAID
- sess.PendingCookMenu = recipesToMenuData(recipes)
- sess.State = net.StateCookRecipe
+ sess.PendingMenu = recipeMenuData(recipes)
+ sess.State = net.StateRecipeChoice
sess.WriteLine(fmt.Sprintf("\nWhat would you like to make with %s on the %s?", itemAName, def.Name))
for i, r := range recipes {
sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
@@ -121,6 +133,22 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
return
}
+ for _, ui := range def.UseInteractions {
+ if ui.Item != itemAID {
+ continue
+ }
+ if ui.Condition != nil && !g.checkCondition(sess, ui.Condition) {
+ continue
+ }
+ if ui.Message != "" {
+ sess.WriteLine(ui.Message)
+ }
+ if ui.Action != nil {
+ g.applyNodeAction(sess, ui.Action)
+ }
+ return
+ }
+
g.StartAction(sess, "use", itemBName)
return
}
@@ -140,12 +168,12 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
matched := g.findCombineResults(sess, p, itemAID, itemBID)
if len(matched) == 1 {
- g.produceCombined(sess, p, matched[0])
+ g.promptHowMany(sess, "combine_"+matched[0].ID)
return
}
if len(matched) > 1 {
- sess.PendingCookMenu = combineMenuData(matched)
- sess.State = net.StateCookRecipe
+ sess.PendingMenu = combineMenuData(matched)
+ sess.State = net.StateRecipeChoice
sess.WriteLine("\nWhat would you like to make?")
for i, def := range matched {
sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.itemColorize(sess, def, def.Name)))
@@ -189,25 +217,6 @@ func (g *Game) findCombineResults(sess *net.Session, p *player.Player, itemAID,
return matched
}
-func (g *Game) produceCombined(sess *net.Session, p *player.Player, def *object.ItemDef) {
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- sess.WriteLine("Your inventory is too full.")
- return
- }
- for _, entry := range def.MadeFrom {
- for _, id := range entry.Items {
- if p.HasItem(id) {
- p.RemoveItem(id, entry.Qty)
- break
- }
- }
- }
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: def.ID, Quantity: 1})
- g.AccountStore.SaveCharacter(p)
- sess.WriteLine(fmt.Sprintf("You combine the ingredients and create %s.", g.itemColorize(sess, def, def.Name)))
-}
-
func combineMenuData(defs []*object.ItemDef) []map[string]string {
out := make([]map[string]string, len(defs))
for i, d := range defs {
diff --git a/internal/game/game.go b/internal/game/game.go
index 49d34b6..87539b1 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -122,10 +122,12 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleTalkInput(sess, input)
case net.StateDropAllConfirm:
g.handleDropAllConfirm(sess, input)
- case net.StateCookRecipe:
- g.handleCookRecipe(sess, input)
- case net.StateSmeltRecipe:
- g.handleSmeltRecipe(sess, input)
+ case net.StateRecipeChoice:
+ g.handleRecipeChoice(sess, input)
+ case net.StateHowMany:
+ g.handleHowMany(sess, input)
+ case net.StateSmithProduct:
+ g.handleSmithProduct(sess, input)
case net.StateColorChoice:
g.handleColorChoice(sess, input)
}
@@ -145,7 +147,7 @@ func classifyCommand(cmd string) CommandClass {
"attack", "kill",
"north", "n", "south", "s", "east", "e",
"west", "w", "up", "u", "down", "d",
- "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt":
+ "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith":
return ClassActive
case "eat":
return ClassFree
@@ -371,6 +373,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
case "smelt":
g.doSmelt(sess, strings.Join(args, " "))
return
+ case "smith":
+ g.doSmith(sess, strings.Join(args, " "))
+ return
case "eat":
g.doEat(sess, strings.Join(args, " "))
return
@@ -459,7 +464,7 @@ func (g *Game) ProcessQueuedCommands() {
}
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking, ActionSmelting:
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing:
default:
if as.Type != ActionMoving || p.MoveTicks <= 0 {
p.ActionState = nil
diff --git a/internal/game/stations.go b/internal/game/stations.go
new file mode 100644
index 0000000..d30a35c
--- /dev/null
+++ b/internal/game/stations.go
@@ -0,0 +1,28 @@
+package game
+
+func (g *Game) findStation(roomID int, stationIDs []string) (defID string, displayName string) {
+ for _, obj := range g.World.AllObjInstances(roomID) {
+ if obj.Depleted {
+ continue
+ }
+ for _, sid := range stationIDs {
+ if obj.DefID == sid {
+ name := sid
+ if def, err := g.ObjectStore.Load(sid); err == nil {
+ name = def.Name
+ }
+ return sid, name
+ }
+ }
+ }
+ return "", ""
+}
+
+func stationMatch(defID string, recipeStations []string) bool {
+ for _, rs := range recipeStations {
+ if rs == defID {
+ return true
+ }
+ }
+ return false
+}