From ec2e94e3463654a6288464a4c070b48ef9bf7368 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 17 Jun 2026 21:13:07 -0400 Subject: feat: smithing added, item/object interaction reworked. recipes and menus refactored. --- internal/game/action.go | 8 +- internal/game/action_cook.go | 153 ------------ internal/game/action_production.go | 467 +++++++++++++++++++++++++++++++++++++ internal/game/action_smelt.go | 153 ------------ internal/game/action_state.go | 9 +- internal/game/cmd_cook.go | 158 ++----------- internal/game/cmd_look.go | 12 +- internal/game/cmd_smelt.go | 163 ++----------- internal/game/cmd_smith.go | 342 +++++++++++++++++++++++++++ internal/game/cmd_use.go | 69 +++--- internal/game/game.go | 17 +- internal/game/stations.go | 28 +++ 12 files changed, 943 insertions(+), 636 deletions(-) delete mode 100644 internal/game/action_cook.go create mode 100644 internal/game/action_production.go delete mode 100644 internal/game/action_smelt.go create mode 100644 internal/game/cmd_smith.go create mode 100644 internal/game/stations.go (limited to 'internal/game') 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 +} -- cgit v1.2.3