aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/action.go2
-rw-r--r--internal/game/action_cook.go153
-rw-r--r--internal/game/action_state.go6
-rw-r--r--internal/game/cmd_attack.go5
-rw-r--r--internal/game/cmd_cook.go246
-rw-r--r--internal/game/cmd_eat.go132
-rw-r--r--internal/game/cmd_use.go200
-rw-r--r--internal/game/game.go26
-rw-r--r--internal/game/help.go2
-rw-r--r--internal/game/tick.go15
10 files changed, 783 insertions, 4 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
index 26484e0..3857030 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -224,6 +224,8 @@ func (g *Game) AdvanceActions() {
g.advanceStoke(sess, p)
case "search":
g.advanceSearch(sess, p)
+ case "cook":
+ g.advanceCook(sess, p)
}
if p.Action == nil {
g.writePrompt(sess)
diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go
new file mode 100644
index 0000000..e4d2597
--- /dev/null
+++ b/internal/game/action_cook.go
@@ -0,0 +1,153 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+
+ "thirdcollapse/internal/action"
+ "thirdcollapse/internal/engine"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/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_state.go b/internal/game/action_state.go
index 280dd56..c5d086e 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -19,6 +19,8 @@ const (
ActionSearching ActionType = "searching"
ActionResting ActionType = "resting"
ActionWalking ActionType = "walking"
+ ActionCooking ActionType = "cooking"
+ ActionEating ActionType = "eating"
)
type ActionState struct {
@@ -64,6 +66,10 @@ func (a *ActionState) Description() string {
return "resting"
case ActionWalking:
return "walking somewhere with a purpose!"
+ case ActionCooking:
+ return "cooking some " + a.TargetName
+ case ActionEating:
+ return "eating " + a.TargetName
}
return ""
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index ba4a2e1..2e15419 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -171,6 +171,11 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
}
func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ if g.processConsumeQueue(p, sess) {
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
+ return
+ }
+
attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
equipAtt := 0
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
new file mode 100644
index 0000000..be5fa3a
--- /dev/null
+++ b/internal/game/cmd_cook.go
@@ -0,0 +1,246 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thirdcollapse/internal/action"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/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 == "" {
+ sess.WriteLine("You need a fire or cooking range to cook.")
+ return
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ if input == "" {
+ g.showCookMenu(sess, p, allRecipes, stationName)
+ return
+ }
+
+ qty, itemName := parseQty(input)
+ _ = qty
+
+ matches := g.findInventoryMatches(itemName, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName))
+ return
+ }
+
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemID := matches[0].ID
+
+ var recipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "cooking" {
+ continue
+ }
+ if stationOK(stationName, 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.")
+ return
+ }
+
+ if len(recipes) == 1 {
+ g.startCook(sess, p, &recipes[0], stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = itemID
+ sess.PendingCookMenu = recipesToMenuData(recipes)
+ sess.State = net.StateCookRecipe
+ 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)))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1))
+}
+
+func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) {
+ seen := make(map[string]bool)
+ var cookables []cookableEntry
+
+ for _, r := range allRecipes {
+ if r.Type != "cooking" {
+ continue
+ }
+ if !stationOK(stationName, r.Station) {
+ continue
+ }
+ if seen[r.ID] {
+ continue
+ }
+ if !r.HasAllItems(p.HasItem) {
+ continue
+ }
+ seen[r.ID] = true
+ cookables = append(cookables, cookableEntry{g.recipeName(sess, &r), r})
+ }
+
+ if len(cookables) == 0 {
+ sess.WriteLine("You don't have anything you can cook.")
+ return
+ }
+
+ if len(cookables) == 1 {
+ g.startCook(sess, p, &cookables[0].Recipe, stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = ""
+ sess.State = net.StateCookRecipe
+ 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))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(cookables)+1))
+
+ sess.PendingCookMenu = makeCookMenuData(cookables)
+}
+
+func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string {
+ if r.Output != "" {
+ if def, err := g.ItemStore.Load(r.Output); err == nil {
+ return g.itemColorize(sess, def, def.Name)
+ }
+ }
+ 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_eat.go b/internal/game/cmd_eat.go
new file mode 100644
index 0000000..5d6292b
--- /dev/null
+++ b/internal/game/cmd_eat.go
@@ -0,0 +1,132 @@
+package game
+
+import (
+ "fmt"
+ "time"
+
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) doEat(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if input == "" {
+ sess.WriteLine("Eat what?")
+ return
+ }
+
+ qty, itemName := parseQty(input)
+ _ = qty
+
+ matches := g.findInventoryMatches(itemName, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName))
+ return
+ }
+
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemID := matches[0].ID
+ def, _ := g.ItemStore.Load(itemID)
+
+ if def == nil || def.EatMessage == "" {
+ sess.WriteLine("You can't eat that.")
+ return
+ }
+
+ if p.ConsumeCooldown > 0 {
+ sess.WriteLine("You're still digesting your last meal.")
+ return
+ }
+
+ g.cancelRest(p.Name)
+
+ if p.Action == nil && len(p.WalkSequence) == 0 {
+ g.doEatNow(sess, p, itemID, def)
+ return
+ }
+
+ g.CancelAction(p)
+
+ g.consumeQueue[p.Name] = &QueuedCommand{
+ Session: sess,
+ Command: "eat",
+ Args: itemID,
+ Timestamp: time.Now(),
+ }
+
+ if !p.OptionBool("queue_silently") {
+ sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", def.Name))
+ }
+}
+
+func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) {
+ if def == nil {
+ var err error
+ def, err = g.ItemStore.Load(itemID)
+ if err != nil || def.EatMessage == "" {
+ return
+ }
+ }
+
+ p.RemoveItem(itemID, 1)
+
+ p.HP += def.HealValue
+ maxHP := p.MaxHP()
+ if p.HP > maxHP {
+ p.HP = maxHP
+ }
+ if p.HP < 0 {
+ p.HP = 0
+ }
+
+ if def.HealValue != 0 {
+ p.StartRegen()
+ }
+
+ p.ConsumeCooldown = 3
+ g.AccountStore.SaveCharacter(p)
+
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name}
+
+ sess.WriteLine(def.EatMessage)
+
+ if p.HP <= 0 {
+ g.endCombat(sess, p, nil)
+ }
+}
+
+func (g *Game) processConsumeQueue(p *player.Player, sess *net.Session) bool {
+ qc, ok := g.consumeQueue[p.Name]
+ if !ok {
+ return false
+ }
+ delete(g.consumeQueue, p.Name)
+
+ itemID := qc.Args
+ def, err := g.ItemStore.Load(itemID)
+ if err != nil || def.EatMessage == "" {
+ return false
+ }
+
+ if p.ConsumeCooldown > 0 {
+ return false
+ }
+
+ if !p.HasItem(itemID) {
+ sess.WriteLine("You no longer have that to eat.")
+ return false
+ }
+
+ g.doEatNow(sess, p, itemID, def)
+ return true
+}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
new file mode 100644
index 0000000..ef36e12
--- /dev/null
+++ b/internal/game/cmd_use.go
@@ -0,0 +1,200 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) doUse(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ g.CancelAction(p)
+
+ lower := strings.ToLower(input)
+
+ sep := ""
+ if strings.Contains(lower, " on ") {
+ sep = " on "
+ } else if strings.Contains(lower, " with ") {
+ sep = " with "
+ }
+
+ if sep != "" {
+ parts := strings.SplitN(lower, sep, 2)
+ itemA := strings.TrimSpace(parts[0])
+ itemB := strings.TrimSpace(parts[1])
+ if itemA == itemB {
+ sess.WriteLine("You can't use that with itself.")
+ return
+ }
+ g.doUseItemOnTarget(sess, p, itemA, itemB)
+ return
+ }
+
+ if len(strings.Fields(input)) >= 2 {
+ g.doUseItemOnTarget(sess, p, strings.Fields(input)[0], strings.Join(strings.Fields(input)[1:], " "))
+ return
+ }
+
+ g.StartAction(sess, "use", input)
+}
+
+func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, itemBName string) {
+ matchesA := g.findInventoryMatches(itemAName, p)
+ if len(matchesA) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemAName))
+ return
+ }
+
+ uniqueA := uniqueItemNames(matchesA)
+ if len(uniqueA) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range uniqueA {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemAID := matchesA[0].ID
+
+ matchesB := g.findInventoryMatches(itemBName, p)
+
+ objInstances := g.World.FindObjInstances(p.RoomID, itemBName)
+ if len(objInstances) > 0 {
+ objSt := &objInstances[0]
+ def, _ := g.ObjectStore.Load(objSt.DefID)
+ if def == nil {
+ g.StartAction(sess, "use", itemBName)
+ return
+ }
+
+ stationIDs := []string{def.ID}
+ recipes, err := g.RecipeStore.FindByStation(itemAID, stationIDs, "")
+ if err == nil && len(recipes) > 0 {
+ if len(recipes) == 1 {
+ g.startCook(sess, p, &recipes[0], def.Name)
+ return
+ }
+ sess.PendingRecipeItem = itemAID
+ sess.PendingCookMenu = recipesToMenuData(recipes)
+ sess.State = net.StateCookRecipe
+ 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)))
+ }
+ return
+ }
+
+ g.StartAction(sess, "use", itemBName)
+ return
+ }
+
+ matchesB = g.findInventoryMatches(itemBName, p)
+ if len(matchesB) > 0 {
+ uniqueB := uniqueItemNames(matchesB)
+ if len(uniqueB) > 1 {
+ sess.WriteLine("Which '" + itemBName + "'?")
+ for _, name := range uniqueB {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemBID := matchesB[0].ID
+
+ matched := g.findCombineResults(sess, p, itemAID, itemBID)
+ if len(matched) == 1 {
+ g.produceCombined(sess, p, matched[0])
+ return
+ }
+ if len(matched) > 1 {
+ sess.PendingCookMenu = combineMenuData(matched)
+ sess.State = net.StateCookRecipe
+ 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)))
+ }
+ return
+ }
+
+ sess.WriteLine("You can't combine those items.")
+ return
+ }
+
+ sess.WriteLine(fmt.Sprintf("There's no '%s' here to use that on.", itemBName))
+}
+
+func (g *Game) findCombineResults(sess *net.Session, p *player.Player, itemAID, itemBID string) []*object.ItemDef {
+ items, err := g.ItemStore.LoadAll()
+ if err != nil {
+ return nil
+ }
+ var matched []*object.ItemDef
+ for _, def := range items {
+ if len(def.MadeFrom) == 0 {
+ continue
+ }
+ aEntry := -1
+ bEntry := -1
+ for ei, entry := range def.MadeFrom {
+ for _, id := range entry.Items {
+ if id == itemAID && aEntry < 0 {
+ aEntry = ei
+ }
+ if id == itemBID && bEntry < 0 {
+ bEntry = ei
+ }
+ }
+ }
+ if aEntry >= 0 && bEntry >= 0 && aEntry != bEntry && madeFromItemsAvailable(p, def.MadeFrom) {
+ matched = append(matched, def)
+ }
+ }
+ 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 {
+ out[i] = map[string]string{"combine_item": d.ID}
+ }
+ return out
+}
+
+func madeFromItemsAvailable(p *player.Player, madeFrom []object.MadeFromEntry) bool {
+ for _, entry := range madeFrom {
+ found := false
+ for _, id := range entry.Items {
+ if p.HasItem(id) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index 456419f..33dd0d1 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -40,6 +40,7 @@ type Game struct {
AccountStore *player.AccountStore
MobStore *world.MobStore
BehaviorStore *action.Store
+ RecipeStore *action.RecipeStore
Hub *net.Hub
Ticks *engine.Engine
WorldFlags map[string]any
@@ -51,6 +52,7 @@ type Game struct {
combatPadWidth int
freeQueue map[string][]QueuedCommand
activeQueue map[string]*QueuedCommand
+ consumeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
}
@@ -62,6 +64,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
AccountStore: player.NewAccountStore(dataDir),
MobStore: world.NewMobStore(dataDir),
BehaviorStore: action.NewStore(dataDir),
+ RecipeStore: action.NewRecipeStore(dataDir),
Ticks: engine.New(),
WorldFlags: make(map[string]any),
ColorConfig: colorConfig,
@@ -70,6 +73,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
loggedInChars: make(map[string]*net.Session),
freeQueue: make(map[string][]QueuedCommand),
activeQueue: make(map[string]*QueuedCommand),
+ consumeQueue: make(map[string]*QueuedCommand),
pendingDepletions: nil,
}
}
@@ -118,6 +122,8 @@ 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)
}
}
@@ -134,8 +140,10 @@ 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":
+ "quit", "use", "burn", "stoke", "search", "walk", "cook":
return ClassActive
+ case "eat":
+ return ClassFree
}
if _, ok := verbAliases[cmd]; ok {
return ClassActive
@@ -314,7 +322,7 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
} else {
g.doHelp(sess, strings.Join(args, " "))
}
- case "mine", "chop", "fish", "cut", "use", "pull", "push":
+ case "mine", "chop", "fish", "cut", "pull", "push":
g.CancelAction(p)
if len(args) == 0 {
target := g.resolveDefaultTarget(p.RoomID, cmd)
@@ -328,6 +336,15 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.StartAction(sess, cmd, strings.Join(args, " "))
return
}
+ case "use":
+ g.doUse(sess, strings.Join(args, " "))
+ return
+ case "cook":
+ g.doCook(sess, strings.Join(args, " "))
+ return
+ case "eat":
+ g.doEat(sess, strings.Join(args, " "))
+ return
case "talk", "speak", "ask":
g.CancelAction(p)
if len(args) == 0 {
@@ -413,7 +430,7 @@ func (g *Game) ProcessQueuedCommands() {
}
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking:
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking:
default:
p.ActionState = nil
}
@@ -455,7 +472,8 @@ func (g *Game) ProcessQueuedCommands() {
g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
}
isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
- if wasBusy || !isBusy {
+ _, isResting := g.restTimers[p.Name]
+ if !isResting && (wasBusy || !isBusy) {
g.writePrompt(qc.Session)
}
}
diff --git a/internal/game/help.go b/internal/game/help.go
index 4d716ad..d3f34c3 100644
--- a/internal/game/help.go
+++ b/internal/game/help.go
@@ -29,8 +29,10 @@ var commandList = []cmdEntry{
{"burn", "Active", "Start a fire"},
{"chop / cut", "Active", "Chop trees (Woodcutting)"},
{"color / colors", "Instant", "Customize display colors"},
+ {"cook", "Active", "Cook raw food on a fire or range"},
{"description / desc", "Instant", "Set your character description"},
{"drop", "Active", "Drop items to the ground"},
+ {"eat", "Free", "Eat food to restore hitpoints"},
{"equipment / eq", "Instant", "Show equipped items"},
{"exits", "Instant", "List available exits"},
{"fish", "Active", "Fish at fishing spots (Fishing)"},
diff --git a/internal/game/tick.go b/internal/game/tick.go
index f326ad8..7567ca7 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -198,3 +198,18 @@ func (g *Game) legalMobExits(inst *world.MobInstance) []int {
}
return legal
}
+
+func (g *Game) ConsumeTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil {
+ continue
+ }
+ if p.ConsumeCooldown > 0 {
+ p.ConsumeCooldown--
+ }
+ }
+}