aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_fletch.go
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/game/cmd_fletch.go
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/game/cmd_fletch.go')
-rw-r--r--internal/game/cmd_fletch.go383
1 files changed, 383 insertions, 0 deletions
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
+}