aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-18 18:50:54 -0400
committerhistoria <[not public]>2026-06-18 18:50:54 -0400
commit17e7725f252c7f5d3be2e9c37d62751c631f196f (patch)
tree40698c5c95cb21302adbb8cd021bd8d049b6fce1 /internal
parentfae979b75e37b6ad57e57062a1b637a37ec87174 (diff)
downloadthehouseoficarus-17e7725f252c7f5d3be2e9c37d62751c631f196f.tar.gz
feat: fletching added including zero-time bolt feathering, which probably will break lots of things
Diffstat (limited to 'internal')
-rw-r--r--internal/action/behavior.go1
-rw-r--r--internal/game/action.go23
-rw-r--r--internal/game/action_burn.go2
-rw-r--r--internal/game/action_fletch.go215
-rw-r--r--internal/game/action_gather.go3
-rw-r--r--internal/game/action_production.go53
-rw-r--r--internal/game/action_state.go3
-rw-r--r--internal/game/cmd_attack.go1
-rw-r--r--internal/game/cmd_cook.go4
-rw-r--r--internal/game/cmd_equipment.go7
-rw-r--r--internal/game/cmd_fletch.go383
-rw-r--r--internal/game/cmd_remove.go102
-rw-r--r--internal/game/cmd_score.go6
-rw-r--r--internal/game/cmd_smelt.go4
-rw-r--r--internal/game/cmd_smith.go103
-rw-r--r--internal/game/cmd_use.go19
-rw-r--r--internal/game/cmd_wear.go106
-rw-r--r--internal/game/game.go17
-rw-r--r--internal/net/server.go1
-rw-r--r--internal/player/player.go7
20 files changed, 978 insertions, 82 deletions
diff --git a/internal/action/behavior.go b/internal/action/behavior.go
index c7ed0c4..67baab0 100644
--- a/internal/action/behavior.go
+++ b/internal/action/behavior.go
@@ -14,7 +14,6 @@ type GatherConfig struct {
FailMsg string `yaml:"fail_message"`
Drops []DropEntry `yaml:"drops"`
RespawnTimer float64 `yaml:"respawn_timer"`
- RespawnMsg string `yaml:"respawn_message"`
RespawnBroadcast string `yaml:"respawn_broadcast"`
DepleteTimer float64 `yaml:"deplete_timer"`
NestChance int `yaml:"nest_chance"`
diff --git a/internal/game/action.go b/internal/game/action.go
index a62ccec..3d0223a 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -53,6 +53,7 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
if p.Action != nil {
g.CancelAction(p)
}
+ g.CancelBackgroundAction(p)
lower := strings.ToLower(target)
instanceIdx := -1
@@ -202,6 +203,11 @@ func (g *Game) CancelAction(p *player.Player) {
p.ClearMoveState()
}
+func (g *Game) CancelBackgroundAction(p *player.Player) {
+ p.BackgroundAction = nil
+ p.BackgroundActionState = nil
+}
+
func (g *Game) AdvanceActions() {
if g.Hub == nil {
return
@@ -238,6 +244,23 @@ func (g *Game) AdvanceActions() {
g.writePrompt(sess)
}
}
+
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p.BackgroundAction == nil {
+ continue
+ }
+ if !p.BackgroundAction.Advance() {
+ continue
+ }
+ switch p.BackgroundAction.Type {
+ case "fletch":
+ g.advanceFletch(sess, p)
+ }
+ if p.BackgroundAction == nil {
+ g.writePrompt(sess)
+ }
+ }
}
func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool {
diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go
index 01bf385..0b7db2c 100644
--- a/internal/game/action_burn.go
+++ b/internal/game/action_burn.go
@@ -15,6 +15,7 @@ func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) {
if p.Action != nil {
g.CancelAction(p)
}
+ g.CancelBackgroundAction(p)
itemID, fromGround, ambiguous := g.resolveBurnTarget(p, input)
if ambiguous {
@@ -33,6 +34,7 @@ func (g *Game) doStoke(sess *net.Session, p *player.Player, input string) {
if p.Action != nil {
g.CancelAction(p)
}
+ g.CancelBackgroundAction(p)
if !g.hasFireObject(p.RoomID) {
sess.WriteLine("There's no fire here to stoke.")
diff --git a/internal/game/action_fletch.go b/internal/game/action_fletch.go
new file mode 100644
index 0000000..734453b
--- /dev/null
+++ b/internal/game/action_fletch.go
@@ -0,0 +1,215 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) startFletchAction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ p.Flags["last_fletch"] = recipe.ID
+ g.AccountStore.SaveCharacter(p)
+
+ if recipe.Wait <= 0 {
+ g.doInstantFletch(sess, p, recipe, count)
+ return
+ }
+
+ g.startTimedFletch(sess, p, recipe, count)
+}
+
+func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+
+ batches := 0
+ totalOutput := 0
+ for {
+ if !g.canDoFletchRecipe(p, recipe) {
+ break
+ }
+
+ placed := false
+ 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 {
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ break
+ }
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
+ }
+
+ if recipe.XP > 0 {
+ if newLevel := p.AddSkillXP(player.Fletching, recipe.XP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d fletching! ***", newLevel)))
+ }
+ }
+
+ batches++
+ totalOutput += outputQty
+
+ if count > 0 {
+ count--
+ if count <= 0 {
+ break
+ }
+ }
+ }
+
+ if batches == 0 {
+ sess.WriteLine("You don't have the materials for that.")
+ return
+ }
+
+ g.AccountStore.SaveCharacter(p)
+
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+
+ msg := fmt.Sprintf("You fletch %d %s.", totalOutput, outputName)
+ if p.OptionBool("xp_drops") && recipe.XP > 0 {
+ totalXP := recipe.XP * batches
+ abbr := player.SkillAbbr[player.Fletching]
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", totalXP, abbr))
+ }
+ sess.WriteLine(msg)
+}
+
+func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+
+ p.BackgroundAction = &action.Action{
+ Type: "fletch",
+ TargetID: recipe.ID,
+ TargetName: outputName,
+ Data: map[string]any{
+ "recipe_id": recipe.ID,
+ "phase": 0,
+ "wait": recipe.Wait,
+ "remaining": count,
+ },
+ WaitLeft: engine.ToTicks(1),
+ }
+ p.BackgroundActionState = &ActionState{Type: ActionFletching, TargetName: outputName}
+
+ sess.WriteLine(fmt.Sprintf("\nYou begin fletching %s.", outputName))
+}
+
+func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
+ recipeID, _ := p.BackgroundAction.Data["recipe_id"].(string)
+ phase, _ := p.BackgroundAction.Data["phase"].(int)
+ wait, _ := p.BackgroundAction.Data["wait"].(float64)
+ remaining, _ := p.BackgroundAction.Data["remaining"].(int)
+
+ recipe := g.loadProductionRecipe(recipeID)
+ if recipe == nil {
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ if phase == 0 {
+ p.BackgroundAction.Data["phase"] = 1
+ p.BackgroundAction.WaitLeft = engine.ToTicks(wait)
+ return
+ }
+
+ if !g.canDoFletchRecipe(p, recipe) {
+ sess.WriteLine("\nYou've run out of fletching materials.")
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ outDef, _ := g.ItemStore.Load(recipe.Output)
+ outputQty := recipe.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+
+ placed := false
+ 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 {
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("\nYour inventory is too full!")
+ g.CancelBackgroundAction(p)
+ return
+ }
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
+ }
+
+ if recipe.XP > 0 {
+ if newLevel := p.AddSkillXP(player.Fletching, recipe.XP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d fletching! ***", newLevel)))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ outputName := recipe.Output
+ if outDef != nil {
+ outputName = outDef.Name
+ }
+ msg := recipe.Message
+ if msg == "" {
+ msg = fmt.Sprintf("You fletch %s.", outputName)
+ }
+ if p.OptionBool("xp_drops") && recipe.XP > 0 {
+ abbr := player.SkillAbbr[player.Fletching]
+ msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr))
+ }
+ sess.WriteLine(msg)
+
+ if remaining > 0 {
+ remaining--
+ p.BackgroundAction.Data["remaining"] = remaining
+ if remaining <= 0 {
+ sess.WriteLine("\nYou've finished fletching.")
+ g.CancelBackgroundAction(p)
+ return
+ }
+ }
+
+ if !g.canContinueProduction(p, recipe) {
+ sess.WriteLine("\nYou've run out of fletching materials.")
+ g.CancelBackgroundAction(p)
+ return
+ }
+
+ p.BackgroundAction.WaitLeft = engine.ToTicks(wait)
+}
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go
index 045c51b..ffd30e1 100644
--- a/internal/game/action_gather.go
+++ b/internal/game/action_gather.go
@@ -203,9 +203,6 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
p.Action.WaitLeft = engine.ToTicks(3)
return
}
- if cfg.RespawnMsg != "" {
- sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg))
- }
p.Action.Data["step"] = 0
p.Action.WaitLeft = engine.ToTicks(wait)
return
diff --git a/internal/game/action_production.go b/internal/game/action_production.go
index 0b8ca45..d4e2274 100644
--- a/internal/game/action_production.go
+++ b/internal/game/action_production.go
@@ -41,6 +41,58 @@ func (g *Game) promptHowMany(sess *net.Session, recipeID string) {
sess.Write("How many (return for all)?: ")
}
+type recipeSelection struct {
+ Recipe *action.RecipeDef
+ Count int
+}
+
+func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) {
+ if input == "" {
+ if lastRecipeID == "" {
+ return sel, "never_mind"
+ }
+ for i := range recipes {
+ if recipes[i].ID == lastRecipeID {
+ sel.Recipe = &recipes[i]
+ return sel, ""
+ }
+ }
+ return sel, "never_mind"
+ }
+
+ qty, productName := parseQty(input)
+ productName = strings.TrimSpace(productName)
+
+ if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) {
+ sel.Recipe = &recipes[idx-1]
+ sel.Count = qty
+ return sel, ""
+ }
+
+ matchCount := 0
+ for i := range recipes {
+ outDef, _ := g.ItemStore.Load(recipes[i].Output)
+ name := recipes[i].Output
+ if outDef != nil {
+ name = outDef.Name
+ }
+ if world.WordPrefixMatch(productName, name) {
+ if matchCount == 0 {
+ sel.Recipe = &recipes[i]
+ sel.Count = qty
+ }
+ matchCount++
+ }
+ }
+ if matchCount > 1 {
+ return sel, "ambiguous"
+ }
+ if sel.Recipe == nil {
+ return sel, "never_mind"
+ }
+ return sel, ""
+}
+
func (g *Game) showMenuTable(sess *net.Session, title string, names []string) {
p := sess.Player.(*player.Player)
tbl := &Table{Title: title}
@@ -129,6 +181,7 @@ func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef {
func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, actionType, displayVerb, startMsg, endMsg string, count int) {
g.CancelAction(p)
+ g.CancelBackgroundAction(p)
if recipe.Skill != "" {
skillLevel := p.Level(player.SkillName(recipe.Skill))
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 26c22f3..4510afd 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -20,6 +20,7 @@ const (
ActionResting ActionType = "resting"
ActionWalking ActionType = "walking"
ActionProducing ActionType = "producing"
+ ActionFletching ActionType = "fletching"
ActionEating ActionType = "eating"
)
@@ -68,6 +69,8 @@ func (a *ActionState) Description() string {
return "walking somewhere with a purpose!"
case ActionProducing:
return a.Verb + " some " + a.TargetName
+ case ActionFletching:
+ return "fletching " + a.TargetName
case ActionEating:
return "eating " + a.TargetName
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index d7fb365..51ac54a 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -111,6 +111,7 @@ func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobIn
func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
g.cancelRest(p.Name)
+ g.CancelBackgroundAction(p)
combat.EnterCombat(p.Name, mob.InstanceID)
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
index b27b293..9b76a84 100644
--- a/internal/game/cmd_cook.go
+++ b/internal/game/cmd_cook.go
@@ -102,6 +102,10 @@ func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []ac
}
if len(entries) == 1 {
+ if p.OptionBool("cook_all") {
+ g.startProductionFromRecipe(sess, p, &entries[0].Recipe, 0)
+ return
+ }
g.promptHowMany(sess, entries[0].Recipe.ID)
return
}
diff --git a/internal/game/cmd_equipment.go b/internal/game/cmd_equipment.go
index 7113880..b256bc3 100644
--- a/internal/game/cmd_equipment.go
+++ b/internal/game/cmd_equipment.go
@@ -4,6 +4,7 @@ import (
"fmt"
"thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
)
@@ -23,6 +24,10 @@ func (g *Game) doEquipment(sess *net.Session) {
if err == nil {
name = def.Name
}
- sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, def, name)))
+ if slot == object.SlotAmmo && p.AmmoQty > 0 {
+ sess.WriteLine(fmt.Sprintf(" %-12s %s (x%d)", slot, g.itemColorize(sess, def, name), p.AmmoQty))
+ } else {
+ sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, def, name)))
+ }
}
}
diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go
new file mode 100644
index 0000000..fe8c0b3
--- /dev/null
+++ b/internal/game/cmd_fletch.go
@@ -0,0 +1,383 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+var fletchLogItems = map[string]bool{
+ "logs": true, "oak_logs": true, "willow_logs": true,
+ "maple_logs": true, "yew_logs": true, "magic_logs": true,
+}
+
+var fletchGemItems = map[string]bool{
+ "uncut_sapphire": true, "uncut_emerald": true,
+ "uncut_ruby": true, "uncut_diamond": true,
+}
+
+func fletchNeedsKnife(r *action.RecipeDef) bool {
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if fletchLogItems[id] {
+ if strings.Contains(r.Output, "shaft") || strings.Contains(r.Output, "bow") {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+func fletchNeedsChisel(r *action.RecipeDef) bool {
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if fletchGemItems[id] {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (g *Game) doFletch(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if combat.GetCombat(p.Name) != nil {
+ sess.WriteLine("You can't do that during combat!")
+ return
+ }
+
+ if p.Action != nil {
+ p.Action = nil
+ p.ActionState = nil
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ var fletchRecipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "fletching" {
+ continue
+ }
+ fletchRecipes = append(fletchRecipes, r)
+ }
+
+ if input != "" {
+ g.doFletchWithInput(sess, p, fletchRecipes, input)
+ return
+ }
+
+ lastRecipeID, _ := p.Flags["last_fletch"].(string)
+ if lastRecipeID != "" {
+ for i := range fletchRecipes {
+ if fletchRecipes[i].ID == lastRecipeID {
+ r := &fletchRecipes[i]
+ if g.canDoFletchRecipe(p, r) {
+ g.startFletchAction(sess, p, r, 0)
+ return
+ }
+ break
+ }
+ }
+ }
+
+ available := g.availableFletchRecipes(p, fletchRecipes)
+ if len(available) == 0 {
+ sess.WriteLine("You don't have any materials to fletch.")
+ return
+ }
+
+ if p.OptionBool("fletch_all") && len(available) == 1 {
+ g.startFletchAction(sess, p, &available[0], 0)
+ return
+ }
+
+ g.showFletchTable(sess, p, available)
+}
+
+func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchRecipes []action.RecipeDef, input string) {
+ qty, productName := parseQty(input)
+ productName = strings.TrimSpace(productName)
+
+ var matched []action.RecipeDef
+ for _, r := range fletchRecipes {
+ outDef, _ := g.ItemStore.Load(r.Output)
+ name := r.Output
+ if outDef != nil {
+ name = outDef.Name
+ }
+ if world.WordPrefixMatch(productName, name) {
+ matched = append(matched, r)
+ }
+ }
+
+ if len(matched) == 0 {
+ sess.WriteLine("You can't fletch that.")
+ return
+ }
+
+ var available []action.RecipeDef
+ for _, r := range matched {
+ if g.canDoFletchRecipe(p, &r) {
+ available = append(available, r)
+ }
+ }
+
+ if len(available) == 0 {
+ sess.WriteLine("You don't have the materials for that.")
+ return
+ }
+
+ if len(available) == 1 {
+ count := qty
+ g.startFletchAction(sess, p, &available[0], count)
+ return
+ }
+
+ g.showFletchTable(sess, p, available)
+}
+
+func (g *Game) hasFletchTools(p *player.Player, r *action.RecipeDef) bool {
+ if fletchNeedsKnife(r) && !g.hasToolType(p, "knife") {
+ return false
+ }
+ if fletchNeedsChisel(r) && !g.hasToolType(p, "chisel") {
+ return false
+ }
+ return true
+}
+
+func (g *Game) canDoFletchRecipe(p *player.Player, r *action.RecipeDef) bool {
+ return p.Level(player.Fletching) >= r.Level &&
+ r.HasAllItemsQty(p.CountItem) &&
+ g.hasFletchTools(p, r)
+}
+
+func (g *Game) availableFletchRecipes(p *player.Player, allFletch []action.RecipeDef) []action.RecipeDef {
+ var result []action.RecipeDef
+ for _, r := range allFletch {
+ if r.HasAllItemsQty(p.CountItem) && g.hasFletchTools(p, &r) {
+ result = append(result, r)
+ }
+ }
+ return result
+}
+
+func (g *Game) showFletchTable(sess *net.Session, p *player.Player, available []action.RecipeDef) {
+ sort.Slice(available, func(i, j int) bool {
+ return available[i].Level < available[j].Level
+ })
+
+ mode := g.colorMode(sess)
+ skillLevel := p.Level(player.Fletching)
+ dimSpec := color.Parse("240")
+
+ tbl := &Table{
+ Title: "Fletching Actions",
+ Columns: []string{"#", "Product", "Materials", "Level"},
+ }
+
+ for i, r := range available {
+ outDef, _ := g.ItemStore.Load(r.Output)
+ productName := r.Output
+ if outDef != nil {
+ productName = outDef.Name
+ }
+
+ outputQty := r.OutputQty
+ if outputQty <= 0 {
+ outputQty = 1
+ }
+ if outDef != nil && outDef.Stackable && outputQty > 1 {
+ productName = fmt.Sprintf("%s x%d", productName, outputQty)
+ }
+
+ var matParts []string
+ for _, e := range r.Consume {
+ itemName := e.Items[0]
+ if def, err := g.ItemStore.Load(e.Items[0]); err == nil {
+ itemName = def.Name
+ }
+ if e.Qty > 1 {
+ matParts = append(matParts, fmt.Sprintf("%s x%d", itemName, e.Qty))
+ } else {
+ matParts = append(matParts, itemName)
+ }
+ }
+ matStr := strings.Join(matParts, ", ")
+ levelStr := fmt.Sprint(r.Level)
+ numStr := fmt.Sprintf("%d", i+1)
+
+ canMake := skillLevel >= r.Level
+ if canMake {
+ productName = g.itemColorize(sess, outDef, productName)
+ } else {
+ productName = color.Render(mode, dimSpec, productName)
+ matStr = color.Render(mode, dimSpec, matStr)
+ levelStr = color.Render(mode, dimSpec, levelStr)
+ numStr = color.Render(mode, dimSpec, numStr)
+ }
+
+ tbl.Rows = append(tbl.Rows, []string{numStr, productName, matStr, levelStr})
+ }
+
+ unicode := p.OptionBool("unicode")
+ sess.WriteLine("")
+ for _, line := range tbl.Render(unicode) {
+ sess.WriteLine(line)
+ }
+
+ lastRecipeID, _ := p.Flags["last_fletch"].(string)
+ hint := ""
+ if lastRecipeID != "" {
+ for _, r := range available {
+ if r.ID == lastRecipeID {
+ if outDef, err := g.ItemStore.Load(r.Output); err == nil {
+ hint = outDef.Name
+ }
+ break
+ }
+ }
+ }
+
+ if hint != "" {
+ sess.Write(fmt.Sprintf("Fletch what (enter for all %s): ", hint))
+ } else {
+ sess.Write("Fletch what: ")
+ }
+
+ sess.PendingMenu = recipeMenuData(available)
+ sess.State = net.StateFletchProduct
+}
+
+func (g *Game) handleFletchProduct(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ menuData := sess.PendingMenu
+ sess.PendingMenu = nil
+ 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 _, entry := range menuData {
+ rid := entry["recipe_id"]
+ for _, r := range allRecipes {
+ if r.ID == rid {
+ recipes = append(recipes, r)
+ break
+ }
+ }
+ }
+
+ if len(recipes) == 0 {
+ g.reprompt(sess)
+ return
+ }
+
+ sort.Slice(recipes, func(i, j int) bool {
+ return recipes[i].Level < recipes[j].Level
+ })
+
+ lastRecipeID, _ := p.Flags["last_fletch"].(string)
+
+ sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID)
+ switch errMsg {
+ case "ambiguous":
+ sess.WriteLine("That's ambiguous.")
+ g.reprompt(sess)
+ return
+ case "never_mind":
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+
+ if !g.canDoFletchRecipe(p, sel.Recipe) {
+ if p.Level(player.Fletching) < sel.Recipe.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d fletching to make that.", sel.Recipe.Level))
+ } else {
+ sess.WriteLine("You don't have the materials for that.")
+ }
+ g.reprompt(sess)
+ return
+ }
+
+ g.startFletchAction(sess, p, sel.Recipe, sel.Count)
+}
+
+func (g *Game) findFletchRecipesForItems(p *player.Player, itemAID, itemBID string) []action.RecipeDef {
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ return nil
+ }
+
+ toolTypeA := ""
+ toolTypeB := ""
+ if defA, _ := g.ItemStore.Load(itemAID); defA != nil {
+ toolTypeA = defA.ToolType
+ }
+ if defB, _ := g.ItemStore.Load(itemBID); defB != nil {
+ toolTypeB = defB.ToolType
+ }
+ isToolA := toolTypeA == "knife" || toolTypeA == "chisel"
+ isToolB := toolTypeB == "knife" || toolTypeB == "chisel"
+
+ var matched []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "fletching" {
+ continue
+ }
+
+ aMatch := false
+ bMatch := false
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if id == itemAID {
+ aMatch = true
+ }
+ if id == itemBID {
+ bMatch = true
+ }
+ }
+ }
+
+ if aMatch && bMatch {
+ matched = append(matched, r)
+ continue
+ }
+
+ if isToolA && bMatch {
+ if (fletchNeedsKnife(&r) && toolTypeA == "knife") ||
+ (fletchNeedsChisel(&r) && toolTypeA == "chisel") {
+ matched = append(matched, r)
+ continue
+ }
+ }
+ if isToolB && aMatch {
+ if (fletchNeedsKnife(&r) && toolTypeB == "knife") ||
+ (fletchNeedsChisel(&r) && toolTypeB == "chisel") {
+ matched = append(matched, r)
+ continue
+ }
+ }
+ }
+ return matched
+}
diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go
index 8f1a413..d36a8fd 100644
--- a/internal/game/cmd_remove.go
+++ b/internal/game/cmd_remove.go
@@ -24,6 +24,8 @@ func (g *Game) doRemove(sess *net.Session, input string) {
g.CancelAction(p)
+ qty, itemName := parseQty(input)
+
var foundSlot object.EquipSlot
var foundItemID string
@@ -32,7 +34,7 @@ func (g *Game) doRemove(sess *net.Session, input string) {
if !ok {
continue
}
- if strings.ToLower(string(slot)) == lower {
+ if strings.ToLower(string(slot)) == strings.ToLower(itemName) {
foundSlot = slot
foundItemID = itemID
break
@@ -41,7 +43,7 @@ func (g *Game) doRemove(sess *net.Session, input string) {
if err != nil {
continue
}
- if def.MatchesName(input) {
+ if def.MatchesName(itemName) {
foundSlot = slot
foundItemID = itemID
break
@@ -53,6 +55,11 @@ func (g *Game) doRemove(sess *net.Session, input string) {
return
}
+ if foundSlot == object.SlotAmmo && p.AmmoQty > 0 {
+ g.removeAmmo(sess, p, qty)
+ return
+ }
+
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
sess.WriteLine("Nowhere to put that!")
@@ -79,6 +86,55 @@ func (g *Game) doRemove(sess *net.Session, input string) {
sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
}
+func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) {
+ ammoID := p.Equipment[object.SlotAmmo]
+ removeQty := p.AmmoQty
+ if qty > 0 && qty < removeQty {
+ removeQty = qty
+ }
+
+ for i := 0; i < 28; i++ {
+ s := p.InvSlot(i)
+ if s != nil && s.ItemID == ammoID {
+ s.Quantity += removeQty
+ p.AmmoQty -= removeQty
+ if p.AmmoQty <= 0 {
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ }
+ g.AccountStore.SaveCharacter(p)
+ def, _ := g.ItemStore.Load(ammoID)
+ name := ammoID
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You unequip %s (x%d).", g.itemColorize(sess, def, name), removeQty))
+ return
+ }
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Nowhere to put that!")
+ return
+ }
+
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: ammoID, Quantity: removeQty})
+ p.AmmoQty -= removeQty
+ if p.AmmoQty <= 0 {
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ def, _ := g.ItemStore.Load(ammoID)
+ name := ammoID
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You unequip %s (x%d).", g.itemColorize(sess, def, name), removeQty))
+}
+
func (g *Game) doRemoveAll(sess *net.Session) {
p := sess.Player.(*player.Player)
@@ -87,7 +143,12 @@ func (g *Game) doRemoveAll(sess *net.Session) {
return
}
- if len(p.Equipment) > p.FreeSlots() {
+ needed := len(p.Equipment)
+ if _, hasAmmo := p.Equipment[object.SlotAmmo]; hasAmmo {
+ needed--
+ needed++
+ }
+ if needed > p.FreeSlots() {
sess.WriteLine("You don't have room to remove everything!")
return
}
@@ -100,6 +161,11 @@ func (g *Game) doRemoveAll(sess *net.Session) {
continue
}
+ if slot == object.SlotAmmo && p.AmmoQty > 0 {
+ g.removeAllAmmo(sess, p, itemID)
+ continue
+ }
+
freeSlot := p.FirstFreeSlot()
delete(p.Equipment, slot)
invSlot := &player.InventorySlot{ItemID: itemID, Quantity: 1}
@@ -122,3 +188,33 @@ func (g *Game) doRemoveAll(sess *net.Session) {
g.AccountStore.SaveCharacter(p)
}
+
+func (g *Game) removeAllAmmo(sess *net.Session, p *player.Player, itemID string) {
+ for i := 0; i < 28; i++ {
+ s := p.InvSlot(i)
+ if s != nil && s.ItemID == itemID {
+ s.Quantity += p.AmmoQty
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ def, _ := g.ItemStore.Load(itemID)
+ name := itemID
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
+ return
+ }
+ }
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot >= 0 {
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: p.AmmoQty})
+ }
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ def, _ := g.ItemStore.Load(itemID)
+ name := itemID
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
+}
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 40d3007..c31433b 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -22,8 +22,10 @@ func (g *Game) doScore(sess *net.Session) {
t := &Table{Title: "Skills", Columns: []string{
color.Render(mode, color.Parse("75"), "Skill"),
+ color.Render(mode, color.Parse("245"), "Abbr"),
color.Render(mode, color.Parse("230"), "Level"),
color.Render(mode, color.Parse("222"), "XP"),
+ color.Render(mode, color.Parse("179"), "XP to Next"),
}}
for _, s := range player.AllSkills {
level := p.Level(s)
@@ -31,8 +33,10 @@ func (g *Game) doScore(sess *net.Session) {
next := player.XPForNextLevel(xp)
t.Rows = append(t.Rows, []string{
color.Render(mode, color.Parse("75"), string(s)),
+ color.Render(mode, color.Parse("245"), player.SkillAbbr[s]),
color.Render(mode, color.Parse("230"), strconv.Itoa(level)),
- color.Render(mode, color.Parse("222"), fmt.Sprintf("%d / %d XP", xp, xp+next)),
+ color.Render(mode, color.Parse("222"), strconv.Itoa(xp)),
+ color.Render(mode, color.Parse("179"), strconv.Itoa(next)),
})
}
for _, line := range t.Render(p.OptionBool("unicode")) {
diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go
index 1cd5436..a5bbc06 100644
--- a/internal/game/cmd_smelt.go
+++ b/internal/game/cmd_smelt.go
@@ -94,6 +94,10 @@ func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []a
}
if len(entries) == 1 {
+ if p.OptionBool("smelt_all") {
+ g.startProductionFromRecipe(sess, p, &entries[0].Recipe, 0)
+ return
+ }
g.promptHowMany(sess, entries[0].Recipe.ID)
return
}
diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go
index 7fc14d2..f2787a5 100644
--- a/internal/game/cmd_smith.go
+++ b/internal/game/cmd_smith.go
@@ -3,7 +3,6 @@ package game
import (
"fmt"
"sort"
- "strconv"
"strings"
"thehouseoficarus/internal/action"
@@ -11,7 +10,6 @@ import (
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
)
func (g *Game) doSmith(sess *net.Session, input string) {
@@ -62,6 +60,12 @@ func (g *Game) doSmith(sess *net.Session, input string) {
}
if len(barTypes) == 1 {
+ if p.OptionBool("smith_all") {
+ if recipe := g.findSingleSmithRecipe(p, barTypes[0], allRecipes); recipe != nil {
+ g.startProductionFromRecipe(sess, p, recipe, 0)
+ return
+ }
+ }
g.showSmithTable(sess, p, barTypes[0], allRecipes)
return
}
@@ -203,72 +207,28 @@ func (g *Game) handleSmithProduct(sess *net.Session, input string) {
return recipes[i].Level < recipes[j].Level
})
- 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.TrimSpace(productName)
-
- if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) {
- targetRecipe = &recipes[idx-1]
- } else {
- matchCount := 0
- for i := range recipes {
- outDef, _ := g.ItemStore.Load(recipes[i].Output)
- name := recipes[i].Output
- if outDef != nil {
- name = outDef.Name
- }
- if world.WordPrefixMatch(productName, name) {
- if matchCount == 0 {
- targetRecipe = &recipes[i]
- }
- matchCount++
- }
- }
- if matchCount > 1 {
- sess.WriteLine("That's ambiguous.")
- g.reprompt(sess)
- return
- }
- }
+ lastFlag := fmt.Sprintf("last_smith_%s", barID)
+ lastRecipeID, _ := p.Flags[lastFlag].(string)
- if targetRecipe == nil {
- sess.WriteLine("Never mind.")
- g.reprompt(sess)
- return
- }
+ sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID)
+ switch errMsg {
+ case "ambiguous":
+ sess.WriteLine("That's ambiguous.")
+ g.reprompt(sess)
+ return
+ case "never_mind":
+ 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))
+ if skillLevel < sel.Recipe.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d smithing to make that.", sel.Recipe.Level))
g.reprompt(sess)
return
}
- if !targetRecipe.HasAllItemsQty(p.CountItem) {
+ if !sel.Recipe.HasAllItemsQty(p.CountItem) {
sess.WriteLine("You don't have enough bars.")
g.reprompt(sess)
return
@@ -277,10 +237,10 @@ func (g *Game) handleSmithProduct(sess *net.Session, input string) {
if p.Flags == nil {
p.Flags = make(map[string]any)
}
- p.Flags[fmt.Sprintf("last_smith_%s", barID)] = targetRecipe.ID
+ p.Flags[lastFlag] = sel.Recipe.ID
g.AccountStore.SaveCharacter(p)
- g.startProductionFromRecipe(sess, p, targetRecipe, count)
+ g.startProductionFromRecipe(sess, p, sel.Recipe, sel.Count)
}
func (g *Game) hasToolType(p *player.Player, toolType string) bool {
@@ -355,6 +315,23 @@ func barMenuData(barIDs []string) []map[string]string {
return out
}
+func (g *Game) findSingleSmithRecipe(p *player.Player, barID string, allRecipes []action.RecipeDef) *action.RecipeDef {
+ var makeable []*action.RecipeDef
+ skillLevel := p.Level(player.Smithing)
+ for i, r := range allRecipes {
+ if r.Type != "smithing" || !r.MatchesEntry(barID) {
+ continue
+ }
+ if skillLevel >= r.Level && r.HasAllItemsQty(p.CountItem) {
+ makeable = append(makeable, &allRecipes[i])
+ }
+ }
+ if len(makeable) == 1 {
+ return makeable[0]
+ }
+ return nil
+}
+
func titleCase(s string) string {
words := strings.Fields(s)
for i, w := range words {
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 7c7bca0..8b8f43e 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
@@ -170,6 +171,24 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
itemBID := matchesB[0].ID
+ fletchRecipes := g.findFletchRecipesForItems(p, itemAID, itemBID)
+ if len(fletchRecipes) > 0 {
+ var available []action.RecipeDef
+ for _, r := range fletchRecipes {
+ if g.canDoFletchRecipe(p, &r) {
+ available = append(available, r)
+ }
+ }
+ if len(available) == 1 {
+ g.startFletchAction(sess, p, &available[0], 0)
+ return
+ }
+ if len(available) > 1 {
+ g.showFletchTable(sess, p, available)
+ return
+ }
+ }
+
matched := g.findCombineResults(sess, p, itemAID, itemBID)
if len(matched) == 1 {
g.promptHowMany(sess, "combine_"+matched[0].ID)
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index 8910e75..ddc970b 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -25,9 +25,11 @@ func (g *Game) doWear(sess *net.Session, input string) {
g.CancelAction(p)
- matches := g.findInventoryMatches(input, p)
+ qty, itemName := parseQty(input)
+
+ matches := g.findInventoryMatches(itemName, p)
if len(matches) == 0 {
- sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input))
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName))
return
}
@@ -52,6 +54,11 @@ func (g *Game) doWear(sess *net.Session, input string) {
return
}
+ if def.EquipSlot == object.SlotAmmo && def.Stackable {
+ g.equipAmmo(sess, p, match.Slot, slot, def, qty)
+ return
+ }
+
existingItemID, slotOccupied := p.Equipment[def.EquipSlot]
if slotOccupied {
@@ -88,13 +95,64 @@ func (g *Game) doWear(sess *net.Session, input string) {
sess.WriteLine(fmt.Sprintf("You %s %s.", verb, g.itemColorize(sess, def, match.Name)))
}
+func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot *player.InventorySlot, def *object.ItemDef, qty int) {
+ transferQty := slot.Quantity
+ if qty > 0 && qty < transferQty {
+ transferQty = qty
+ }
+
+ existingAmmoID, hasAmmo := p.Equipment[object.SlotAmmo]
+ if hasAmmo && existingAmmoID != slot.ItemID {
+ g.unequipAmmo(p)
+ }
+
+ if slot.Quantity <= transferQty {
+ p.SetInvSlot(invIdx, nil)
+ } else {
+ slot.Quantity -= transferQty
+ }
+
+ if hasAmmo && existingAmmoID == slot.ItemID {
+ p.AmmoQty += transferQty
+ } else {
+ p.Equipment[object.SlotAmmo] = slot.ItemID
+ p.AmmoQty = transferQty
+ }
+
+ g.AccountStore.SaveCharacter(p)
+ displayName := g.itemColorize(sess, def, def.Name)
+ sess.WriteLine(fmt.Sprintf("You equip %s (x%d).", displayName, p.AmmoQty))
+}
+
+func (g *Game) unequipAmmo(p *player.Player) {
+ ammoID, ok := p.Equipment[object.SlotAmmo]
+ if !ok || p.AmmoQty <= 0 {
+ return
+ }
+ for i := 0; i < 28; i++ {
+ s := p.InvSlot(i)
+ if s != nil && s.ItemID == ammoID {
+ s.Quantity += p.AmmoQty
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ return
+ }
+ }
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot >= 0 {
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: ammoID, Quantity: p.AmmoQty})
+ }
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+}
+
func (g *Game) doWearAll(sess *net.Session) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
bestPerSlot := make(map[object.EquipSlot]struct {
- itemID string
- value int
+ itemID string
+ value int
invSlot int
})
@@ -108,12 +166,16 @@ func (g *Game) doWearAll(sess *net.Session) {
continue
}
current, exists := bestPerSlot[def.EquipSlot]
- if !exists || def.Value > current.value {
+ val := def.Value
+ if def.EquipSlot == object.SlotAmmo && def.Stackable {
+ val = def.Value * slot.Quantity
+ }
+ if !exists || val > current.value {
bestPerSlot[def.EquipSlot] = struct {
- itemID string
- value int
+ itemID string
+ value int
invSlot int
- }{slot.ItemID, def.Value, i}
+ }{slot.ItemID, val, i}
}
}
@@ -125,7 +187,33 @@ func (g *Game) doWearAll(sess *net.Session) {
var equipped []string
for eqSlot, best := range bestPerSlot {
if existing, ok := p.Equipment[eqSlot]; ok && existing == best.itemID {
- continue
+ if eqSlot != object.SlotAmmo {
+ continue
+ }
+ }
+
+ if eqSlot == object.SlotAmmo {
+ bestDef, _ := g.ItemStore.Load(best.itemID)
+ if bestDef != nil && bestDef.Stackable {
+ invSlot := p.InvSlot(best.invSlot)
+ if invSlot == nil {
+ continue
+ }
+ existingID, hasAmmo := p.Equipment[object.SlotAmmo]
+ if hasAmmo && existingID != best.itemID {
+ g.unequipAmmo(p)
+ }
+ transferQty := invSlot.Quantity
+ p.SetInvSlot(best.invSlot, nil)
+ if hasAmmo && existingID == best.itemID {
+ p.AmmoQty += transferQty
+ } else {
+ p.Equipment[object.SlotAmmo] = best.itemID
+ p.AmmoQty = transferQty
+ }
+ equipped = append(equipped, g.itemColorize(sess, bestDef, bestDef.Name))
+ continue
+ }
}
if existing, ok := p.Equipment[eqSlot]; ok {
diff --git a/internal/game/game.go b/internal/game/game.go
index 87539b1..ef81fca 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -128,6 +128,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleHowMany(sess, input)
case net.StateSmithProduct:
g.handleSmithProduct(sess, input)
+ case net.StateFletchProduct:
+ g.handleFletchProduct(sess, input)
case net.StateColorChoice:
g.handleColorChoice(sess, input)
}
@@ -149,7 +151,7 @@ func classifyCommand(cmd string) CommandClass {
"west", "w", "up", "u", "down", "d",
"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith":
return ClassActive
- case "eat":
+ case "eat", "fletch":
return ClassFree
}
if _, ok := verbAliases[cmd]; ok {
@@ -379,6 +381,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
case "eat":
g.doEat(sess, strings.Join(args, " "))
return
+ case "fletch":
+ g.doFletch(sess, strings.Join(args, " "))
+ return
case "talk", "speak", "ask":
g.CancelAction(p)
if len(args) == 0 {
@@ -477,6 +482,16 @@ func (g *Game) ProcessQueuedCommands() {
if !ok || p == nil {
continue
}
+ if p.BackgroundAction == nil {
+ p.BackgroundActionState = nil
+ }
+ }
+
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil {
+ continue
+ }
cmds := g.freeQueue[p.Name]
for _, qc := range cmds {
g.cancelRest(p.Name)
diff --git a/internal/net/server.go b/internal/net/server.go
index bb14b04..e74f15f 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -34,6 +34,7 @@ const (
StateRecipeChoice
StateHowMany
StateSmithProduct
+ StateFletchProduct
StateColorChoice
)
diff --git a/internal/player/player.go b/internal/player/player.go
index e7d98ac..edfe5dd 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -127,6 +127,10 @@ var OptionDefs = []OptionDef{
{"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"},
{"visual_tick_count", OptInt, 0, nil, "Cycle length for tick counter (0 = no counter)"},
{"visual_tick_text", OptString, "Tick", nil, "Text displayed for visual ticks"},
+ {"fletch_all", OptBool, false, nil, "Auto-start fletching when only one product is possible"},
+ {"smith_all", OptBool, false, nil, "Auto-start smithing when only one product is possible"},
+ {"cook_all", OptBool, false, nil, "Auto-start cooking when only one product is possible"},
+ {"smelt_all", OptBool, false, nil, "Auto-start smelting when only one product is possible"},
}
var optionByName map[string]*OptionDef
@@ -157,8 +161,11 @@ type Player struct {
Options map[string]any `yaml:"-"`
Flags map[string]any `yaml:"flags"`
RegenerateTick int
+ AmmoQty int `yaml:"ammo_qty,omitempty"`
Action *action.Action `yaml:"-"`
ActionState any `yaml:"-"`
+ BackgroundAction *action.Action `yaml:"-"`
+ BackgroundActionState any `yaml:"-"`
WalkSequence []string `yaml:"-"`
ConsumeCooldown int `yaml:"-"`
MoveTicks int `yaml:"-"`