aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/items/adamantite_bar.yaml6
-rw-r--r--data/items/bronze_bar.yaml6
-rw-r--r--data/items/gold_bar.yaml6
-rw-r--r--data/items/iron_bar.yaml6
-rw-r--r--data/items/mithril_bar.yaml6
-rw-r--r--data/items/runite_bar.yaml6
-rw-r--r--data/items/silver_bar.yaml6
-rw-r--r--data/items/steel_bar.yaml6
-rw-r--r--data/objects/furnace.yaml5
-rw-r--r--data/recipes/smelt_adamantite.yaml15
-rw-r--r--data/recipes/smelt_bronze.yaml15
-rw-r--r--data/recipes/smelt_gold.yaml13
-rw-r--r--data/recipes/smelt_iron.yaml17
-rw-r--r--data/recipes/smelt_mithril.yaml15
-rw-r--r--data/recipes/smelt_runite.yaml15
-rw-r--r--data/recipes/smelt_silver.yaml13
-rw-r--r--data/recipes/smelt_steel.yaml15
-rw-r--r--data/rooms/12.yaml17
-rw-r--r--internal/action/recipe.go20
-rw-r--r--internal/game/action.go2
-rw-r--r--internal/game/action_smelt.go153
-rw-r--r--internal/game/action_state.go3
-rw-r--r--internal/game/cmd_look.go125
-rw-r--r--internal/game/cmd_smelt.go230
-rw-r--r--internal/game/cmd_use.go33
-rw-r--r--internal/game/cmd_wear.go9
-rw-r--r--internal/game/game.go30
-rw-r--r--internal/net/server.go1
-rw-r--r--internal/player/player.go10
-rw-r--r--internal/world/world.go6
30 files changed, 760 insertions, 50 deletions
diff --git a/data/items/adamantite_bar.yaml b/data/items/adamantite_bar.yaml
new file mode 100644
index 0000000..5a9b827
--- /dev/null
+++ b/data/items/adamantite_bar.yaml
@@ -0,0 +1,6 @@
+id: adamantite_bar
+name: adamantite bar
+color: "71"
+description: "A bar of adamantite metal, extremely durable."
+value: 200
+stackable: false
diff --git a/data/items/bronze_bar.yaml b/data/items/bronze_bar.yaml
new file mode 100644
index 0000000..053cfe8
--- /dev/null
+++ b/data/items/bronze_bar.yaml
@@ -0,0 +1,6 @@
+id: bronze_bar
+name: bronze bar
+color: "178"
+description: "A bar of bronze metal, smelted from copper and tin ore."
+value: 4
+stackable: false
diff --git a/data/items/gold_bar.yaml b/data/items/gold_bar.yaml
new file mode 100644
index 0000000..3272b04
--- /dev/null
+++ b/data/items/gold_bar.yaml
@@ -0,0 +1,6 @@
+id: gold_bar
+name: gold bar
+color: "220"
+description: "A bar of gold metal."
+value: 50
+stackable: false
diff --git a/data/items/iron_bar.yaml b/data/items/iron_bar.yaml
new file mode 100644
index 0000000..26c8352
--- /dev/null
+++ b/data/items/iron_bar.yaml
@@ -0,0 +1,6 @@
+id: iron_bar
+name: iron bar
+color: "250"
+description: "A bar of iron metal."
+value: 12
+stackable: false
diff --git a/data/items/mithril_bar.yaml b/data/items/mithril_bar.yaml
new file mode 100644
index 0000000..00e97a4
--- /dev/null
+++ b/data/items/mithril_bar.yaml
@@ -0,0 +1,6 @@
+id: mithril_bar
+name: mithril bar
+color: "75"
+description: "A bar of mithril metal, light and strong."
+value: 100
+stackable: false
diff --git a/data/items/runite_bar.yaml b/data/items/runite_bar.yaml
new file mode 100644
index 0000000..1faff69
--- /dev/null
+++ b/data/items/runite_bar.yaml
@@ -0,0 +1,6 @@
+id: runite_bar
+name: runite bar
+color: "39"
+description: "A bar of runite metal, the finest alloy known."
+value: 500
+stackable: false
diff --git a/data/items/silver_bar.yaml b/data/items/silver_bar.yaml
new file mode 100644
index 0000000..eccb25f
--- /dev/null
+++ b/data/items/silver_bar.yaml
@@ -0,0 +1,6 @@
+id: silver_bar
+name: silver bar
+color: "255"
+description: "A bar of silver metal."
+value: 20
+stackable: false
diff --git a/data/items/steel_bar.yaml b/data/items/steel_bar.yaml
new file mode 100644
index 0000000..450fd5c
--- /dev/null
+++ b/data/items/steel_bar.yaml
@@ -0,0 +1,6 @@
+id: steel_bar
+name: steel bar
+color: "254"
+description: "A bar of steel, forged from iron ore and coal."
+value: 30
+stackable: false
diff --git a/data/objects/furnace.yaml b/data/objects/furnace.yaml
new file mode 100644
index 0000000..437842a
--- /dev/null
+++ b/data/objects/furnace.yaml
@@ -0,0 +1,5 @@
+id: furnace
+name: furnace
+color: "196"
+props:
+ description: "A blazing hot furnace for smelting ores into bars."
diff --git a/data/recipes/smelt_adamantite.yaml b/data/recipes/smelt_adamantite.yaml
new file mode 100644
index 0000000..c5a34f7
--- /dev/null
+++ b/data/recipes/smelt_adamantite.yaml
@@ -0,0 +1,15 @@
+id: smelt_adamantite
+type: smelting
+skill: smithing
+level: 65
+xp: 37
+wait: 4
+station: [furnace]
+consume:
+ - items: [adamantite_ore]
+ qty: 1
+ - items: [coal]
+ qty: 6
+output: adamantite_bar
+message: "You remove a white hot adamantite bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_bronze.yaml b/data/recipes/smelt_bronze.yaml
new file mode 100644
index 0000000..bb46da5
--- /dev/null
+++ b/data/recipes/smelt_bronze.yaml
@@ -0,0 +1,15 @@
+id: smelt_bronze
+type: smelting
+skill: smithing
+level: 1
+xp: 6
+wait: 4
+station: [furnace]
+consume:
+ - items: [copper_ore]
+ qty: 1
+ - items: [tin_ore]
+ qty: 1
+output: bronze_bar
+message: "You remove a white hot bronze bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_gold.yaml b/data/recipes/smelt_gold.yaml
new file mode 100644
index 0000000..6f30147
--- /dev/null
+++ b/data/recipes/smelt_gold.yaml
@@ -0,0 +1,13 @@
+id: smelt_gold
+type: smelting
+skill: smithing
+level: 45
+xp: 22
+wait: 4
+station: [furnace]
+consume:
+ - items: [gold_ore]
+ qty: 1
+output: gold_bar
+message: "You remove a white hot gold bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_iron.yaml b/data/recipes/smelt_iron.yaml
new file mode 100644
index 0000000..2631b28
--- /dev/null
+++ b/data/recipes/smelt_iron.yaml
@@ -0,0 +1,17 @@
+id: smelt_iron
+type: smelting
+skill: smithing
+level: 20
+xp: 12
+wait: 4
+station: [furnace]
+consume:
+ - items: [iron_ore]
+ qty: 1
+output: iron_bar
+message: "You remove a white hot iron bar!"
+fail_message: "The iron is too impure to make a workable bar."
+success:
+ base: 0.5
+ per_level: 0
+ cap: 0.5
diff --git a/data/recipes/smelt_mithril.yaml b/data/recipes/smelt_mithril.yaml
new file mode 100644
index 0000000..0e2856a
--- /dev/null
+++ b/data/recipes/smelt_mithril.yaml
@@ -0,0 +1,15 @@
+id: smelt_mithril
+type: smelting
+skill: smithing
+level: 50
+xp: 30
+wait: 4
+station: [furnace]
+consume:
+ - items: [mithril_ore]
+ qty: 1
+ - items: [coal]
+ qty: 4
+output: mithril_bar
+message: "You remove a white hot mithril bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_runite.yaml b/data/recipes/smelt_runite.yaml
new file mode 100644
index 0000000..1c2935e
--- /dev/null
+++ b/data/recipes/smelt_runite.yaml
@@ -0,0 +1,15 @@
+id: smelt_runite
+type: smelting
+skill: smithing
+level: 80
+xp: 50
+wait: 4
+station: [furnace]
+consume:
+ - items: [runite_ore]
+ qty: 1
+ - items: [coal]
+ qty: 8
+output: runite_bar
+message: "You remove a white hot runite bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_silver.yaml b/data/recipes/smelt_silver.yaml
new file mode 100644
index 0000000..1630452
--- /dev/null
+++ b/data/recipes/smelt_silver.yaml
@@ -0,0 +1,13 @@
+id: smelt_silver
+type: smelting
+skill: smithing
+level: 25
+xp: 14
+wait: 4
+station: [furnace]
+consume:
+ - items: [silver_ore]
+ qty: 1
+output: silver_bar
+message: "You remove a white hot silver bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/recipes/smelt_steel.yaml b/data/recipes/smelt_steel.yaml
new file mode 100644
index 0000000..e3df4b6
--- /dev/null
+++ b/data/recipes/smelt_steel.yaml
@@ -0,0 +1,15 @@
+id: smelt_steel
+type: smelting
+skill: smithing
+level: 40
+xp: 17
+wait: 4
+station: [furnace]
+consume:
+ - items: [iron_ore]
+ qty: 1
+ - items: [coal]
+ qty: 2
+output: steel_bar
+message: "You remove a white hot steel bar!"
+fail_message: "You fail to smelt a usable bar."
diff --git a/data/rooms/12.yaml b/data/rooms/12.yaml
index c9ab894..4da9c7e 100644
--- a/data/rooms/12.yaml
+++ b/data/rooms/12.yaml
@@ -1,6 +1,21 @@
id: 12
name: "Forge"
-description: "A blazing forge and anvil dominate this smoky workshop. This area is not yet accessible."
+description: "A blazing forge and anvil dominate this smoky workshop. The heat from the furnace is intense."
exits:
east: 13
west: 11
+objects:
+ - id: furnace
+spawns:
+ - item_id: copper_ore
+ quantity: 3
+ respawn_ticks: 30
+ - item_id: tin_ore
+ quantity: 3
+ respawn_ticks: 30
+ - item_id: iron_ore
+ quantity: 3
+ respawn_ticks: 30
+ - item_id: coal
+ quantity: 5
+ respawn_ticks: 30
diff --git a/internal/action/recipe.go b/internal/action/recipe.go
index 7627047..18c08c5 100644
--- a/internal/action/recipe.go
+++ b/internal/action/recipe.go
@@ -196,6 +196,26 @@ func (r *RecipeDef) ConsumeAll(hasItem func(string) bool, removeItem func(string
return true
}
+func (r *RecipeDef) HasAllItemsQty(countItem func(string) int) bool {
+ for _, e := range r.Consume {
+ found := false
+ for _, id := range e.Items {
+ qty := e.Qty
+ if qty <= 0 {
+ qty = 1
+ }
+ if countItem(id) >= qty {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
+
func (r *RecipeDef) HasAllItems(hasItem func(string) bool) bool {
for _, e := range r.Consume {
found := false
diff --git a/internal/game/action.go b/internal/game/action.go
index c08295c..608528d 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -227,6 +227,8 @@ func (g *Game) AdvanceActions() {
g.advanceSearch(sess, p)
case "cook":
g.advanceCook(sess, p)
+ case "smelt":
+ g.advanceSmelt(sess, p)
}
if p.Action == nil {
g.writePrompt(sess)
diff --git a/internal/game/action_smelt.go b/internal/game/action_smelt.go
new file mode 100644
index 0000000..75fb8a0
--- /dev/null
+++ b/internal/game/action_smelt.go
@@ -0,0 +1,153 @@
+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 b28e170..6ce3734 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -20,6 +20,7 @@ const (
ActionResting ActionType = "resting"
ActionWalking ActionType = "walking"
ActionCooking ActionType = "cooking"
+ ActionSmelting ActionType = "smelting"
ActionEating ActionType = "eating"
)
@@ -68,6 +69,8 @@ func (a *ActionState) Description() string {
return "walking somewhere with a purpose!"
case ActionCooking:
return "cooking some " + a.TargetName
+ case ActionSmelting:
+ return "smelting some " + a.TargetName
case ActionEating:
return "eating " + a.TargetName
}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index 8cadfd3..77c8776 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -214,27 +214,24 @@ func (g *Game) doLook(sess *net.Session) {
ground := g.World.GroundItemsDetailed(p.RoomID)
if len(ground) > 0 {
- sort.Slice(ground, func(i, j int) bool {
- ni := ground[i].ItemID
- if def, err := g.ItemStore.Load(ground[i].ItemID); err == nil {
- ni = def.Name
- }
- nj := ground[j].ItemID
- if def, err := g.ItemStore.Load(ground[j].ItemID); err == nil {
- nj = def.Name
- }
- return strings.ToLower(ni) < strings.ToLower(nj)
- })
+ showDespawn := p.OptionBool("despawn")
+ showReserve := p.OptionBool("reserve")
- sess.WriteLine("")
- sess.WriteLine("On the ground:")
+ type displayLine struct {
+ name string
+ quantity int
+ colorName string
+ annotation string
+ }
- type groundLine struct {
- prefix string
- reserved string
+ type groupKey struct {
+ itemID string
+ despawnTimer int
}
- var lines []groundLine
- maxPrefix := 0
+
+ var lines []displayLine
+ groups := make(map[groupKey]*displayLine)
+ var groupOrder []groupKey
for _, info := range ground {
def, err := g.ItemStore.Load(info.ItemID)
@@ -243,37 +240,89 @@ func (g *Game) doLook(sess *net.Session) {
name = def.Name
}
coloredName := g.itemColorize(sess, def, name)
- prefix := ""
- if info.Quantity > 1 {
- prefix = fmt.Sprintf(" %d x %s", info.Quantity, coloredName)
- } else {
- prefix = fmt.Sprintf(" %s", coloredName)
- }
- if visibleLen(prefix) > maxPrefix {
- maxPrefix = visibleLen(prefix)
- }
- var parts []string
+
if info.ReservedFor != "" {
- if p.OptionBool("reserve") {
+ var parts []string
+ if showReserve {
parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer))
} else {
parts = append(parts, "reserved")
}
+ if showDespawn && !info.IsSpawn {
+ parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer))
+ }
+ annotation := ""
+ if len(parts) > 0 {
+ annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", "))
+ }
+ lines = append(lines, displayLine{
+ name: name,
+ quantity: info.Quantity,
+ colorName: coloredName,
+ annotation: annotation,
+ })
+ continue
+ }
+
+ timerKey := -1
+ if showDespawn && !info.IsSpawn {
+ timerKey = info.DespawnTimer
+ }
+ key := groupKey{itemID: info.ItemID, despawnTimer: timerKey}
+
+ if existing, ok := groups[key]; ok {
+ existing.quantity += info.Quantity
+ } else {
+ annotation := ""
+ if showDespawn && !info.IsSpawn {
+ annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer)
+ }
+ dl := &displayLine{
+ name: name,
+ quantity: info.Quantity,
+ colorName: coloredName,
+ annotation: annotation,
+ }
+ groups[key] = dl
+ groupOrder = append(groupOrder, key)
}
- if p.OptionBool("despawn") && !info.IsSpawn {
- parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer))
+ }
+
+ for _, key := range groupOrder {
+ lines = append(lines, *groups[key])
+ }
+
+ sort.Slice(lines, func(i, j int) bool {
+ return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name)
+ })
+
+ sess.WriteLine("")
+ sess.WriteLine("On the ground:")
+
+ type fmtLine struct {
+ prefix string
+ annotation string
+ }
+ var fmtLines []fmtLine
+ maxPrefix := 0
+
+ for _, dl := range lines {
+ prefix := ""
+ if dl.quantity > 1 {
+ prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName)
+ } else {
+ prefix = fmt.Sprintf(" %s", dl.colorName)
}
- reserved := ""
- if len(parts) > 0 {
- reserved = fmt.Sprintf(" (%s)", strings.Join(parts, ", "))
+ if visibleLen(prefix) > maxPrefix {
+ maxPrefix = visibleLen(prefix)
}
- lines = append(lines, groundLine{prefix, reserved})
+ fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation})
}
- for _, l := range lines {
- if l.reserved != "" {
+ for _, l := range fmtLines {
+ if l.annotation != "" {
pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix))
- sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.reserved))
+ sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation))
} else {
sess.WriteLine(l.prefix)
}
diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go
new file mode 100644
index 0000000..a308352
--- /dev/null
+++ b/internal/game/cmd_smelt.go
@@ -0,0 +1,230 @@
+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 == "" {
+ sess.WriteLine("You need a furnace to smelt.")
+ return
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ if input == "" {
+ g.showSmeltMenu(sess, p, allRecipes, stationName)
+ return
+ }
+
+ 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
+ }
+
+ itemID := matches[0].ID
+
+ var recipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "smelting" {
+ continue
+ }
+ if !smeltStationOK(stationName, r.Station) {
+ continue
+ }
+ if !r.MatchesEntry(itemID) {
+ continue
+ }
+ if !r.HasAllItemsQty(p.CountItem) {
+ continue
+ }
+ skillLevel := p.Level(player.SkillName(r.Skill))
+ if skillLevel < r.Level {
+ continue
+ }
+ recipes = append(recipes, r)
+ }
+
+ if len(recipes) == 0 {
+ sess.WriteLine("You can't smelt that here.")
+ return
+ }
+
+ if len(recipes) == 1 {
+ g.startSmelt(sess, p, &recipes[0], stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = itemID
+ sess.PendingCookMenu = smeltRecipesToMenuData(recipes)
+ sess.State = net.StateSmeltRecipe
+ 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)))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1))
+}
+
+func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) {
+ seen := make(map[string]bool)
+ var smeltables []smeltableEntry
+
+ 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) {
+ continue
+ }
+ skillLevel := p.Level(player.SkillName(r.Skill))
+ if skillLevel < r.Level {
+ continue
+ }
+ seen[r.ID] = true
+ smeltables = append(smeltables, smeltableEntry{g.recipeName(sess, &r), r})
+ }
+
+ if len(smeltables) == 0 {
+ sess.WriteLine("You don't have anything you can smelt.")
+ return
+ }
+
+ if len(smeltables) == 1 {
+ g.startSmelt(sess, p, &smeltables[0].Recipe, stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = ""
+ sess.State = net.StateSmeltRecipe
+ 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}
+ }
+ return out
+}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 56aa5a1..247df72 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -74,6 +74,39 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
stationIDs := []string{def.ID}
recipes, err := g.RecipeStore.FindByStation(itemAID, stationIDs, "")
if err == nil && len(recipes) > 0 {
+ if recipes[0].Type == "smelting" {
+ filtered := recipes[:0:0]
+ for _, r := range recipes {
+ if r.Type != "smelting" {
+ continue
+ }
+ if !r.HasAllItemsQty(p.CountItem) {
+ continue
+ }
+ skillLevel := p.Level(player.SkillName(r.Skill))
+ if skillLevel < r.Level {
+ continue
+ }
+ filtered = append(filtered, r)
+ }
+ if len(filtered) == 0 {
+ sess.WriteLine("You can't smelt that here.")
+ return
+ }
+ if len(filtered) == 1 {
+ g.startSmelt(sess, p, &filtered[0], def.Name)
+ return
+ }
+ sess.PendingRecipeItem = itemAID
+ sess.PendingCookMenu = smeltRecipesToMenuData(filtered)
+ sess.State = net.StateSmeltRecipe
+ 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)))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(filtered)+1))
+ return
+ }
if len(recipes) == 1 {
g.startCook(sess, p, &recipes[0], def.Name)
return
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index cbc2ff3..8910e75 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -31,6 +31,15 @@ func (g *Game) doWear(sess *net.Session, input string) {
return
}
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Equip what?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
match := matches[0]
slot := p.InvSlot(match.Slot)
if slot == nil {
diff --git a/internal/game/game.go b/internal/game/game.go
index 5af35be..49d34b6 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -124,6 +124,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleDropAllConfirm(sess, input)
case net.StateCookRecipe:
g.handleCookRecipe(sess, input)
+ case net.StateSmeltRecipe:
+ g.handleSmeltRecipe(sess, input)
case net.StateColorChoice:
g.handleColorChoice(sess, input)
}
@@ -132,18 +134,18 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
func classifyCommand(cmd string) CommandClass {
switch cmd {
case "say", "score", "sc", "inventory", "i", "inv",
- "equipment", "eq", "look", "l", "exits", "help",
+ "look", "l", "exits", "help",
"map", "option", "options", "alias", "unalias",
"description", "desc", "queued", "color", "colors",
"colortable", "prompt", "style":
return ClassInstant
- case "wear", "wield", "remove", "unwear", "unwield":
+ case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
case "get", "take", "grab", "pick", "drop",
"attack", "kill",
"north", "n", "south", "s", "east", "e",
"west", "w", "up", "u", "down", "d",
- "quit", "use", "burn", "stoke", "search", "walk", "cook":
+ "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt":
return ClassActive
case "eat":
return ClassFree
@@ -188,6 +190,16 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
parts := strings.Fields(strings.ToLower(input))
cmd := parts[0]
+
+ if len(parts) == 1 {
+ switch cmd {
+ case "equip", "eq", "equipment", "wear", "wield":
+ g.doEquipment(sess)
+ g.writePrompt(sess)
+ return
+ }
+ }
+
class := classifyCommand(cmd)
if class == ClassInstant {
@@ -302,8 +314,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doScore(sess)
case "i", "inv", "inventory":
g.doInventory(sess)
- case "eq", "equipment":
- g.doEquipment(sess)
case "quit":
g.doQuit(sess)
return
@@ -358,6 +368,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
case "cook":
g.doCook(sess, strings.Join(args, " "))
return
+ case "smelt":
+ g.doSmelt(sess, strings.Join(args, " "))
+ return
case "eat":
g.doEat(sess, strings.Join(args, " "))
return
@@ -394,7 +407,7 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
case "walk":
g.doWalk(sess, args)
return
- case "wear", "wield":
+ case "eq", "equipment", "equip", "wear", "wield":
g.doWear(sess, strings.Join(args, " "))
return
case "remove", "unwear", "unwield":
@@ -446,7 +459,7 @@ func (g *Game) ProcessQueuedCommands() {
}
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking:
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking, ActionSmelting:
default:
if as.Type != ActionMoving || p.MoveTicks <= 0 {
p.ActionState = nil
@@ -488,13 +501,12 @@ func (g *Game) ProcessQueuedCommands() {
if p.MoveTicks > 0 {
p.ClearMoveState()
}
- wasBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
if len(parts) > 0 {
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 || p.MoveTicks > 0
_, isResting := g.restTimers[p.Name]
- if !isResting && (wasBusy || !isBusy) {
+ if !isResting && !isBusy {
g.writePrompt(qc.Session)
}
}
diff --git a/internal/net/server.go b/internal/net/server.go
index 203539c..f22c398 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -32,6 +32,7 @@ const (
StateTalk
StateDropAllConfirm
StateCookRecipe
+ StateSmeltRecipe
StateColorChoice
)
diff --git a/internal/player/player.go b/internal/player/player.go
index 48c9359..e7d98ac 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -338,6 +338,16 @@ func (p *Player) HasItem(itemID string) bool {
return false
}
+func (p *Player) CountItem(itemID string) int {
+ count := 0
+ for _, slot := range p.Inventory {
+ if slot != nil && slot.ItemID == itemID {
+ count += slot.Quantity
+ }
+ }
+ return count
+}
+
func (p *Player) RemoveItem(itemID string, qty int) bool {
remaining := qty
for i, slot := range p.Inventory {
diff --git a/internal/world/world.go b/internal/world/world.go
index 7768739..9bed76e 100644
--- a/internal/world/world.go
+++ b/internal/world/world.go
@@ -482,7 +482,8 @@ func (w *World) SeedGroundItems(roomID int) {
merged := false
for _, e := range w.groundItems[roomID] {
if e.quantity > 0 && strings.EqualFold(e.itemID, s.ItemID) &&
- (e.reserveTimer <= 0 || e.reservedFor == "") {
+ (e.reserveTimer <= 0 || e.reservedFor == "") &&
+ e.despawnTimer <= 0 {
e.quantity += s.Quantity
e.respawnQty += s.Quantity
e.isSpawn = true
@@ -516,7 +517,8 @@ func (w *World) Tick() {
e.quantity = e.respawnQty
for _, other := range entries {
if other != e && other.quantity > 0 && strings.EqualFold(other.itemID, e.itemID) &&
- (other.reserveTimer <= 0 || other.reservedFor == "") {
+ (other.reserveTimer <= 0 || other.reservedFor == "") &&
+ other.despawnTimer <= 0 {
e.quantity += other.quantity
other.quantity = 0
}